2-2. 보안 기능 가이드 (Java Only)

인증, 인가, 암호화, 세션 관리 등 애플리케이션의 핵심 보안 기능 구현 시 발생하는 취약점 및 안전한 코드 가이드입니다.


1. 적절한 인증 없는 중요 기능 허용

중요 기능이나 자원을 요청할 때 인증 여부를 확인하지 않아 중요 정보가 노출되거나 권한이 도용될 수 있습니다.

Java - 취약한 코드권한 확인 없음
/* 취약한 코드 (Java) */
@RequestMapping(value = "/modify.do", method = RequestMethod.POST)
public ModelAndView memberModifyProcess(@ModelAttribute("MemberModel") MemberModel memberModel, HttpServletRequest request, HttpSession session) {
    // 1. 로그인한 사용자를 불러온다.
    String userId = (String) session.getAttribute("userId"); // 로그인 ID
    String requestedUserId = memberModel.getUserId(); // 요청 ID

    // 로그인한 사용자와 요청한 사용자 ID 일치 확인 누락 
    if (service.modifyMember(memberModel)) { 
        // 회원정보 수정 로직
    } 
    // 만약 requestedUserId가 다른 사용자 ID라면 IDOR 발생
}
Java - 안전한 코드Fix: 요청 사용자 확인
/* 안전한 코드 (Java) */
@RequestMapping(value = "/modify.do", method = RequestMethod.POST)
public ModelAndView memberModifyProcess(@ModelAttribute("MemberModel") MemberModel memberModel, HttpServletRequest request, HttpSession session) {
    String userId = (String) session.getAttribute("userId"); 
    String requestedUserId = memberModel.getUserId();

    // 요청된 자원이 로그인된 사용자의 것인지 확인
    if (!requestedUserId.equals(userId)) {
        // 권한 없음 에러 처리
        throw new SecurityException("접근 권한이 없습니다.");
    }
    if (service.modifyMember(memberModel)) {
        // 안전하게 회원정보 수정
    }
}

2. 부적절한 인가

프로그램이 모든 실행 경로에 대해 접근 제어를 검사하지 않거나 불완전하게 검사하여 비인가자가 정보를 유출할 수 있습니다.

Java - 취약한 코드인가 확인 없는 중요 기능 실행
/* 취약한 코드 (Java) */
private BoardDao boardDao;
String action = request.getParameter("action");
String contentId = request.getParameter("contentId");

// 요청을 하는 사용자의 delete 작원 권한 확인 없이 수행하고 있어 안전하지 않다.
if (action != null && action.equals("delete")) {
    boardDao.delete(contentId);
}
Java - 안전한 코드Fix: 인가 확인 로직
/* 안전한 코드 (Java) */
private BoardDao boardDao;
String action = request.getParameter("action");
String contentId = request.getParameter("contentId");
User user = (User)session.getAttribute("user");

// 사용자정보에서 해당 사용자가 delete작업의 권한이 있는지 확인한 뒤 삭제 작업을 수행한다.
// checkAccessControlList(user, action) 로직을 통해 권한을 검사해야 함
if (action != null && action.equals("delete") && checkAccessControlList(user, action)) {
    boardDao.delete(contentId);
}

3. 중요한 자원에 대한 잘못된 권한 설정

중요 자원(설정 파일 등)에 대해 최소 권한이 아닌 과도한 권한이 부여되어 비인가자에게 노출됩니다.

Java - 취약한 코드전체 사용자 권한 허용
/* 취약한 코드 (Java) */
File file = new File("/home/setup/system.ini");
// 모든 사용자에게 실행, 읽기, 쓰기 권한을 허용하여 안전하지 않다.
file.setExecutable(true, false); // 모든 사용자 실행 허용
file.setReadable(true, false);   // 모든 사용자 읽기 허용
file.setWritable(true, false);   // 모든 사용자 쓰기 허용
Java - 안전한 코드Fix: 최소 권한 할당
/* 안전한 코드 (Java) */
File file = new File("/home/setup/system.ini");
// 소유자에게만 권한을 할당 (최소 권한의 원칙)
file.setExecutable(false); // 소유자 실행 권한 금지 (필요 시 true)
file.setReadable(true);    // 소유자 읽기 권한 허용
file.setWritable(false);   // 소유자 쓰기 권한 금지 (필요 시 true)

4. 취약한 암호화 알고리즘 사용

DES, RC5, MD5 등 정보보호 측면에서 취약한 암호화 알고리즘을 사용하여 중요 정보의 기밀성을 보장할 수 없습니다.

Java - 취약한 코드DES 알고리즘 사용
/* 취약한 코드 (Java) */
public byte[] encrypt(byte[] msg, Key k) { 
    byte[] rslt = null;
    try {
        // 키 길이가 짧아 취약한 암호화 알고리즘인 DES를 사용하여 안전하지 않다.
        Cipher c = Cipher.getInstance("DES");
        c.init(Cipher.ENCRYPT_MODE, k);
        rslt = c.update(msg);
    } catch (/* ... */) { /* ... */ }
    return rslt;
}
Java - 안전한 코드Fix: AES 알고리즘 사용
/* 안전한 코드 (Java) */
public byte[] encrypt(byte[] msg, Key k) { 
    byte[] rslt = null;
    try {
        // 키 길이가 길어 강력한 알고리즘인 AES를 사용하여 안전하다.
        Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
        c.init(Cipher.ENCRYPT_MODE, k); 
        rslt = c.update(msg);
    } catch (/* ... */) { /* ... */ }
    return rslt;
}

5. 암호화되지 않은 중요정보

중요 정보(개인정보, 패스워드 등)를 평문으로 **저장**하거나 **전송**하여 인가되지 않은 사용자에게 노출됩니다.

Java - 취약한 코드평문 저장
/* 취약한 코드 (Java) */
String pwd = request.getParameter("pwd");
// ... (SQL 쿼리)
stmt.setString(2, pwd); // 입력받은 패스워드가 평문으로 DB에 저장되어 안전하지 않다.
stmt.executeUpdate();
Java - 안전한 코드Fix: 해시 + Salt 저장
/* 안전한 코드 (Java) */
String pwd = request.getParameter("pwd");
// 패스워드를 솔트값을 포함하여 SHA-256 해쉬로 변경하여 안전하게 저장한다.
MessageDigest md = MessageDigest.getInstance("SHA-256");
// md.update(salt); // 솔트 적용 로직
byte[] hashInBytes = md.digest(pwd.getBytes());
// ... (해시값을 문자열로 변환)
pwd = sb.toString(); 
// ...
stmt.setString(2, pwd);
stmt.executeUpdate();
Java - 취약한 코드평문 전송
/* 취약한 코드 (Java) */
try {
    Socket s = new Socket("taranis", 4444);
    PrintWriter o = new PrintWriter(s.getOutputStream(), true);
    // 패스워드를 평문으로 전송하여 안전하지 않다.
    String password = getPassword();
    o.write(password);
} catch (/* ... */) { /* ... */ }
Java - 안전한 코드Fix: 암호화 전송
/* 안전한 코드 (Java) */
// 패스워드를 암호화 하여 전송
try {
    Socket s = new Socket("taranis", 4444);
    PrintStream o = new PrintStream(s.getOutputStream(), true);
    // 패스워드를 강력한 AES암호화 알고리즘으로 암호화하여 전송
    Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
    String password = getPassword();
    byte[] encPassword = c.update(password.getBytes());
    o.write(encPassword, 0, encPassword.length);
} catch (/* ... */) { /* ... */ }

6. 하드코드된 중요정보

소스코드 내부에 패스워드, 암호화 키 등 중요 정보를 상수로 직접 코딩하여 소스코드 유출 시 함께 노출됩니다.

Java - 취약한 코드DB 정보 하드코딩
/* 취약한 코드 (Java) */
public class MemberDAO {
    // ...
    private static final String USER = "SCOTT"; // DB ID;
    private static final String PASS = "SCOTT"; // DB PW;
    // ...
    con = DriverManager.getConnection(URL, USER, PASS); // DB 패스워드가 소스코드에 평문으로 저장
}
Java - 안전한 코드Fix: 외부 파일/복호화
/* 안전한 코드 (Java) */
public class MemberDAO {
    // ...
    public Connection getConn() {
        // ...
        // 암호화된 패스워드를 프로퍼티에서 읽어들여 복호화해서 사용해야한다.
        String PASS = props.getProperty("EncryptedPswd");
        byte[] decryptedPswd = cipher.doFinal(PASS.getBytes());
        PASS = new String(decryptedPswd);
        con = DriverManager.getConnection(URL, USER, PASS);
        // ...
    }
}
Java - 취약한 코드암호화 키 하드코딩
/* 취약한 코드 (Java) */
public String encriptString(String usr) {
    // 암호화 키를 소스코드 내부에 사용하는 것은 안전하지 않다.
    String key = "22df3023sf~2;asn!@#/>as"; 
    if (key != null) {
        byte[] bToEncrypt = usr.getBytes("UTF-8");
        SecretKeySpec sKeySpec = new SecretKeySpec(key.getBytes(), "AES");
    }
    // ...
}
Java - 안전한 코드Fix: 외부 파일/복호화된 키 사용
/* 안전한 코드 (Java) */
public String encriptString(String usr) {
    // 암호화 키는 외부 파일에서 암호화 된 형태로 저장하고, 사용 시 복호화 한다.
    String key = getPassword("./password.ini");
    key = decrypt(key);
    if (key != null) {
        byte[] bToEncrypt = usr.getBytes("UTF-8");
        SecretKeySpec sKeySpec = new SecretKeySpec(key.getBytes(), "AES");
    }
    // ...
}

7. 충분하지 않은 키 길이 사용

키 길이가 짧으면 짧은 시간 안에 키를 찾아낼 수 있고 이를 이용해 공격자가 암호화된 데이터나 패스워드를 복호화 할 수 있게 됩니다.

Java - 취약한 코드짧은 키 길이 사용
/* 취약한 코드 (Java) */
public static void generateKey() {
    try {
        final KeyPairGenerator keyGen = KeyPairGenerator.getInstance(ALGORITHM);
        // RSA 키 길이를 1024 비트로 짧게 설정하는 경우 안전하지 않다.
        keyGen.initialize(1024);
        final KeyPair key = keyGen.generateKeyPair();
        // ...
    } catch (/* ... */) { /* ... */ }
}
Java - 안전한 코드Fix: 2048 비트 이상
/* 안전한 코드 (Java) */
public static void generateKey() {
    try {
        final KeyPairGenerator keyGen = KeyPairGenerator.getInstance(ALGORITHM);
        // 공개키 암호화에 사용하는 키의 길이는 적어도 2048비트 이상으로 설정한다.
        keyGen.initialize(2048);
        final KeyPair key = keyGen.generateKeyPair();
        // ...
    } catch (/* ... */) { /* ... */ }
}

8. 적절하지 않은 난수 값 사용

예측 불가능한 난수가 필요한 상황(세션 ID, 암호화 키)에서 예측 가능한 난수(`java.util.Random`)를 사용하여 공격에 취약해집니다.

Java - 취약한 코드Random 클래스 사용
/* 취약한 코드 (Java) */
import java.util.Random;
public Static String getAuthKey() {
    // 보안결정을 위한 난수로는 안전하지 않은 Random 클래스 사용
    Random random = new Random();
    String authKey = Integer.toString(random.nextInt());
    // ...
}
Java - 안전한 코드Fix: SecureRandom 클래스 사용
/* 안전한 코드 (Java) */
import java.security.SecureRandom;
import java.security.MessageDigest;
public Static String getAuthKey() {
    // 보안결정을 위한 난수로는 예측이 거의 불가능하게 암호학적으로 보호된 SecureRandom을 사용한다.
    try {
        SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        secureRandom.setSeed(secureRandom.generateSeed(128));
        String authKey = new String(digest.digest((secureRandom.nextLong() + "").getBytes()));
    } catch (/* ... */) { /* ... */ }
    return authKey;
}

9. 취약한 비밀번호 허용

강한 비밀번호 조합 규칙(영문, 숫자, 특수문자 등)이 미흡하여 무차별 대입 공격(Brute Force)에 취약해집니다.

Java - 취약한 코드복잡도 검증 누락
/* 취약한 코드 (Java) */
String pass = request.getParameter("pass");
UserVo userVO = new UserVo(id, pass);
// 비밀번호의 자릿수, 특수문자 포함 여부 등 복잡도를 체크하지 않고 등록
String result = registerDAO.register(userVO);
Java - 안전한 코드Fix: 정규식 복잡도 검사
/* 안전한 코드 (Java) */
String pass = request.getParameter("pass");
// 비밀번호에 자릿수, 특수문자 포함 여부 등의 복잡도를 체크하고 등록하게 한다.
Pattern pattern = Pattern.compile("((?=.*[a-zA-Z])(?=.*[0-9@#$%]).{9, })");
Matcher matcher = pattern.matcher(pass);

if (!matcher.matches()) {
    return "비밀번호 조합규칙 오류";
}
UserVo userVO = new UserVo(id, pass);
String result = registerDAO.register(userVO);

10. 부적절한 전자서명 확인

전자서명을 검증하지 않거나 검증 절차가 부적절하면 위변조된 파일/코드가 실행될 수 있습니다.

Java - 취약한 코드서명 확인 없이 사용
/* 취약한 코드 (Java) */
File f = new File(downloadedFilePath);
// 신뢰할 수 없는 곳에서 다운로드 한 JAR 파일의 서명을 확인하지 않고 사용
JarFile jf = new JarFile(f); 
Java - 안전한 코드Fix: 전자서명 확인
/* 안전한 코드 (Java) */
File f = new File(downloadedFilePath);
// JarFile 생성자에 boolean형 파라미터를 사용하여 전자서명을 확인
JarFile jf = new JarFile(f, true); 
Enumeration ens = jf.entries();
while (ens.hasMoreElements()) {
    JarEntry en = ens.nextElement();
    if (!en.isDirectory()) {
        // ...
        CodeSigner[] signers = en.getCodeSigners(); // 서명 주체를 확인
    }
}

11. 부적절한 인증서 유효성 검증

SSL/TLS 통신 시 서버 측 인증서의 유효성을 확인하지 않거나 부적절하게 검증하여, 악성 호스트에 연결되거나 중간자 공격에 노출됩니다.

Java - 취약한 코드인증서 유효성 검증 로직 누락
/* 취약한 코드 (Java) */
// PDF에 Java 코드 예제가 없어 C 코드 기반으로 유추
// (인증서 유효성 검증 없이 통신을 진행하는 로직)
URL url = new URL("https://untrusted.com/");
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setHostnameVerifier( (hostname, session) -> true ); // 모든 호스트명 허용 (매우 위험)
InputStream in = conn.getInputStream();
//...
Java - 안전한 코드Fix: 인증서 체인 및 유효기간 확인
/* 안전한 코드 (Java) */
private boolean verifySignature(X509Certificate toVerify, X509Certificate signingCert) {
    /* 검증하려는 호스트 인증서(toVerify)와 CA인증서(signingCert)의 DN이 일치하는지 여부를 확인한다.*/
    if (!toVerify.getIssuerDN().equals(signingCert.getSubjectDN())) return false;
    try {
        // 호스트 인증서가 CA인증서로 서명 되었는지 확인한다.
        toVerify.verify(signingCert.getPublicKey());
        // 호스트 인증서가 유효기간이 만료되었는지 확인한다.
        toVerify.checkValidity();
        return true;
    } catch (GeneralSecurityException verifyFailed) {
        return false;
    }
}

12. 사용자 하드디스크에 저장되는 쿠키를 통한 정보노출

개인정보, 인증 정보 등이 영속적인 쿠키(Persistent Cookie)에 저장되도록 만료 시간을 길게 설정하여 공격자에게 노출됩니다.

Java - 취약한 코드과도한 만료시간
/* 취약한 코드 (Java) */
Cookie loginCookie = new Cookie("rememberme", "YES");
// 쿠키의 만료시간을 1년으로 과도하게 길게 설정하고 있어 안전하지 않다.
loginCookie.setMaxAge(60*60*24*365);
response.addCookie(loginCookie);
Java - 안전한 코드Fix: 최소 만료시간
/* 안전한 코드 (Java) */
Cookie loginCookie = new Cookie("rememberme", "YES");
// 쿠키의 만료시간은 해당 기능에 맞춰 최소로 사용한다. (예: 24시간)
loginCookie.setMaxAge(60*60*24);
response.addCookie(loginCookie);

13. 주석문 안에 포함된 시스템 주요정보

개발자가 편의를 위해 ID, 패스워드 등 보안 관련 내용을 주석문에 포함하여 소스코드 유출 시 함께 노출됩니다.

Java - 취약한 코드주석 내 중요정보
/* 취약한 코드 (Java) */
// 주석문으로 DB연결 ID, 패스워드의 중요한 정보를 노출시켜 안전하지 않다. 
// DB연결 root / a1q2w3r3f2!@
con = DriverManager.getConnection(URL, USER, PASS);
Java - 안전한 코드Fix: 주석 제거
/* 안전한 코드 (Java) */
// ID, 패스워드등의 중요 정보는 주석에 포함해서는 안된다.
con = DriverManager.getConnection(URL, USER, PASS);

14. 솔트 없이 일방향 해시 함수 사용

패스워드 저장 시 솔트(Salt) 없이 해시하여 레인보우 테이블 공격에 취약해집니다.

Java - 취약한 코드솔트 없이 해시
/* 취약한 코드 (Java) */
public String getPasswordHash(String password) throws Exception  {
    MessageDigest md = MessageDigest.getInstance("SHA-256");
    // 해쉬에 솔트를 적용하지 않아 안전하지 않다.
    md.update(password.getBytes());
    byte byteData[] = md.digest();
    // ...
}
Java - 안전한 코드Fix: Salt 사용
/* 안전한 코드 (Java) */
public String getPasswordHash(String password, byte[] salt) throws Exception {
    MessageDigest md = MessageDigest.getInstance("SHA-256");
    md.update(password.getBytes());
    // 해쉬 사용 시에는 원문을 찾을 수 없도록 솔트를 사용하여야 한다.
    md.update(salt);
    byte byteData[] = md.digest();
    // ...
}

15. 무결성 검사 없는 코드 다운로드

원격에서 소스코드나 실행파일을 다운로드받아 실행 시 무결성 검사를 하지 않아 악의적인 코드가 실행됩니다.

Java - 취약한 코드무결성 검증 누락
/* 취약한 코드 (Java) */
URL[] classURLs = new URL[] { new URL("file:subdir/") };
// 원격에서 파일을 다운로드한 뒤 로드하면서, 대상 파일에 대한 무결성 검사를 수행하지 않아 파일변조 등으로 인한 피해가 발생할 수 있다.
URLClassLoader loader = new URLClassLoader(classURLs);
Class loadedClass = Class.forName("LoadMe", true, loader);
Java - 안전한 코드Fix: 암호화/서명 확인 후 로드
/* 안전한 코드 (Java) */
// ... (파일을 다운로드 받은 후)
byte[] loadFile = FileManager.getBytes(jarFile);
// Public Key로 복호화 (전자 서명 확인)
loadFile = decrypt(loadFile, publicKey);
// 복호화된 파일을 생성한다.
FileManager.createFile(loadFile, jarFile);

URLClassLoader loader = new URLClassLoader(classURLs);
Class loadedClass = Class.forName("MyClass", true, loader);

16. 반복된 인증시도 제한 기능 부재

인증 시도 횟수를 제한하지 않아 공격자가 무차별 대입 공격(Brute Force)을 통해 계정 권한을 획득할 수 있습니다.

Java - 취약한 코드시도 횟수 제한 없음
/* 취약한 코드 (Java) */
private static final int FAIL = -1;
public void login() {
    // ...
    int result = FAIL;
    try {
        // 인증 실패에 대해 제한을 두지 않아 안전하지 않다.
        while (result == FAIL) {
            // ...
            result = verifyUser(username, password);
        }
    } catch (/* ... */) { /* ... */ }
}
Java - 안전한 코드Fix: 횟수 제한/잠금
/* 안전한 코드 (Java) */
private static final int FAIL = -1;
private static final int MAX_ATTEMPTS = 5; // 최대 시도 횟수 설정
public void login() {
    // ...
    int result = FAIL;
    int count = 0;
    try {
        // 인증 실패 및 시도 횟수에 제한을 두어 안전하다.
        while (result == FAIL && count < MAX_ATTEMPTS) {
            // ...
            result = verifyUser(username, password);
            count++;
        }
    } catch (/* ... */) { /* ... */ }
}