시큐어코딩 점검툴 사용법 / 보안 취약점 보완 조치 방법

시큐어코딩 점검은 신규 서비스 안정화 기간 중 실시하고, 매년 정기적으로 점검을 실시합니다.


시큐어코딩 진단 방법

CODE-RAY XG PRO 사용법

제공 받은 exe 파일을 설치하여 실행 후 운영 브랜치를 바라보는 프로젝트폴더\src\main 경로로 지정합니다.
프로그램 기본 세팅 확인 후 우측 검사 시작 버튼을 누르면 30분 정도 소요됩니다.
시큐어코딩 점검 계획서에 첨부할 사진으로 점검 전, 점검 후, 취약점 보완 및 재점검 후 각각 캡쳐해두어야 합니다.

CODE-RAY 기본 세팅

  • 제외 파일 필터 : .html, *.js, *.cubrid.xml, *.mysql.xml, java.xml, pom.xml, sql*.xml
  • 프리셋 목록 : 행안부 선택
  • 파서 선택 : javascript, jsp, html, xml, java 체크
  • 예외 항목 : 외부에서 제공받은 유틸 및 API 연동 파일 제외 (롤 관리 메뉴에서 추가)

CODE-RAY 단점
제외된 폴더 내 파일들도 모두 검사해서 시간이 오래 걸립니다.


개선 불가 코드 제외 처리

전자정부프레임워크(egovframework 폴더), 라이브러리 폴더 등은 수정할 수 없으니 제외처리 해야 합니다.

폴더 제외 방법

상단 룰 관리 탭 > 예외 룰 관리 > 추가 > 예외 이름 작성 > 예외 항목 추가 > 폴더 > 검사 결과의 파일별 리스트 참고해서 제외할 폴더 경로를 /로 구분하여 작성 (다중 폴더 추가 가능) > 추가 > 저장 > 저장

제외 폴더 예시

  • java/egovframework/*
  • resources/egovframework/*
  • webapp/WEB-INF/jsp/egovframework/*

파일 제외 방법

상단 룰 관리 탭 > 예외 룰 관리 > 추가 > 예외 이름 작성 > 예외 항목 추가 > 파일 > 검사 결과 상세 목록 검출 파일 참고해서 제외할 파일명 작성 > 추가 > 저장 > 저장
제외 폴더 필터로 제외되지 않은 파일은 새 예외 항목을 만들어서 제외합니다.

제외 파일 예시

  • GeneratorController.java
  • GeneratorServiceImpl.java
  • IMFileUtil.java

보안 취약점 보완 방법

시큐어코딩 점검 결과에서 낮음 등급은 제외하고 높음, 보통 등급만 보완합니다.
보안약점 설명 및 수정 방법을 참고하고, 구글링 해서 수정 후 계속 프로그램을 돌려보면서 확인합니다.
개발서버용 브랜치에서 먼저 수정 후 운영서버용 브랜치에 반영해야 합니다.

HTTP 응답 분할

JAVA 컨트롤러에서 HTTP 요청에 들어있는 파라미터를 HTTP 응답 헤더에 포함하여 다시 전달할 때,
입력값에 개행문자가 존재하면 HTTP 응답이 2개 이상으로 분리될 수 있습니다.
공격자가 개행문자를 이용하여 첫 번째 응답을 종료시키고, 두 번째 응답을 조작해서 공격할 수 있습니다.

String 쿠키값변수 = null;
Cookie[] cookies = req.getCookies();

// 쿠키 키 값 존재 여부 확인
if (cookies != null) {
    for (Cookie cookie : cookies) {
        if ("쿠키키".equals(cookie.getName())) {
            쿠키값변수 = cookie.getValue();

            // 개행 문자 제거 (필터링 적용)
            if (쿠키값변수 != null) {
              쿠키값변수 = 쿠키값변수.replaceAll("[\r\n]", "");
            }
        }
    }
}

// 쿠키가 없으면 새로운 쿠키 생성
if (쿠키값변수 == null) {
  Cookie newCookie = new Cookie("쿠키키", 쿠키값변수);
  res.addCookie(newCookie);
}

// VO 입력 및 INSERT 처리

위와 같이, 개행 문자 (CR : \r, LF : \n) 필터링으로 보안 공격을 방어할 수 있습니다.

중요정보 평문전송 (HTTP)

HTTPS 보안 채널의 경우, 쿠키 객체에 setSecure 함수로 보안 속성을 설정하여 중요 정보 노출을 방지해야 합니다.
보안 속성이 설정된 쿠키는 HTTP에서 전송되지 않으므로, HTTPS 여부 체크 후 설정하는 것이 좋습니다.

Cookie cookie = new Cookie("쿠키키", 쿠키값);

// 운영 HTTPS인 경우, 보안 속성 적용
boolean isProduction = req.getScheme().equals("https");
cookie.setSecure(isProduction);

res.addCookie(cookie);

운영서버 https에서는 보안 속성 적용, 개발서버 http에서는 보안 속성 미적용하는 코드 예시입니다.

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

보안에 취약한 DES 암호화 코드

public static String encryptString(String str, String encryptKey) throws Exception {
  if (str == null || str.length() == 0) {
    return "";
  }

  Cipher cipher = Cipher.getInstance("DESede/ECB/PKCS5Padding");
  SecretKeySpec keySpec = new SecretKeySpec(encryptKey.getBytes(), "DESede");
  cipher.init(Cipher.ENCRYPT_MODE, keySpec);

  byte[] plainText = str.getBytes("UTF-8");
  byte[] cipherText = cipher.doFinal(plainText);
  return encodeBase64Escape(cipherText);
}

public static String decryptString(String str, String decryptKey) throws Exception {
  if (str == null || str.length() == 0) {
    return "";
  }
  Cipher cipher = Cipher.getInstance("DESede/ECB/PKCS5Padding");
  SecretKeySpec keySpec = new SecretKeySpec(decryptKey.getBytes(), "DESede");
  cipher.init(Cipher.DECRYPT_MODE, keySpec);

  byte[] cipherText = cipher.doFinal(decodeBase64Escape(str));
  return new String(cipherText, "UTF-8");
}

AES 암호화 코드

public static String encryptString(String str, String encryptKey) throws Exception {
  if (str == null || str.length() == 0) {
    return "";
  }

  Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
  SecretKeySpec keySpec = new SecretKeySpec(encryptKey.getBytes(), "AES");
  cipher.init(Cipher.ENCRYPT_MODE, keySpec);

  byte[] plainText = str.getBytes("UTF-8");
  byte[] cipherText = cipher.doFinal(plainText);
  return encodeBase64Escape(cipherText);
}

public static String decryptString(String str, String decryptKey) throws Exception {
  if (str == null || str.length() == 0) {
    return "";
  }
  Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
  SecretKeySpec keySpec = new SecretKeySpec(decryptKey.getBytes(), "AES");
  cipher.init(Cipher.DECRYPT_MODE, keySpec);

  byte[] cipherText = cipher.doFinal(decodeBase64Escape(str));
  return new String(cipherText, "UTF-8");
}

취약하다고 알려진 DES, RC5 등의 알고리즘을 대신하여 AES, SEED 등의 안전한 알고리즘으로 대체하여 사용합니다.
AES ECB 모드는 보안에 취약하므로 CBC 모드로 변경하고, IV(초기화 벡터)를 추가하면 보안이 더 강화됩니다.

Private 배열에 Public 데이터 할당

Public 파라미터를 Private 배열에 저장하면, 외부에서 Private 배열에 접근할 수 있습니다.
파라미터 변수의 레퍼런스가 아닌 값 자체를 할당해야 정보 은닉이 가능합니다.

private 배열 할당 개선

private String[] 배열명;

public void set배열명(String[] 배열명) {
  this.배열명 = 배열명;
}

아래와 같이, 새로운 배열을 생성하고 값을 복사하면 보안이 강화됩니다.

public void set배열명(String[] 배열명) {
  if (배열명 != null) {
    this.배열명 = new String[배열명.length];
    System.arraycopy(배열명, 0, this.배열명, 0, 배열명.length);
  } else {
    this.배열명 = null;
  }
}

System.arraycopy 함수를 사용하면 for문보다 빠르고 효율적으로 배열을 복사할 수 있습니다.

public void set배열명(String[] 배열명) {
  this.배열명 = (배열명 == null) ? null : Arrays.copyOf(배열명, 배열명.length);
}

Arrays.copyOf 함수는 System.arraycopy 함수와 같은 기능을 하면서 더 간결합니다.
null 체크도 삼항연산자로 해서 코드 길이를 더 간결하게 개선하였습니다.

private List 할당 개선 (얕은 복사)</mark>

private List<VO명> 리스트명;

public void set리스트명(List<VO명> List명) {
  this.리스트명 = 리스트명;
}

아래와 같이, 새로운 리스트 객체를 생성하고 값을 복사하면 보안이 강화됩니다.

public void set리스트명(List<VO명> 리스트명) {
  this.리스트명 = (리스트명 == null) ? null : new ArrayList<>(리스트명);
}

이렇게만 변경해도 CODE-RAY에서는 보안 취약점이 제거되는 것을 확인할 수 있습니다.
얕은 복사 시 내부 값은 참조되므로, 깊은 복사를 해야 보안상 더 유리합니다.

private List 할당 개선 (깊은 복사)</mark>

public class VO명 implements Cloneable {
    private String 변수1;
    private String 변수2;

    public VO명(VO명 vo) {
        this.변수1 = vo.변수1;
        this.변수2 = vo.변수2;
    }

    @Override
    public VO명 clone() {
        return new VO명(this);
    }
}

위 VO 코드 예시처럼 생성자를 통해 각 변수 값을 복사하는 clone 함수를 정의하면 더 좋습니다.

public void set리스트명(List<VO명> 리스트명) {
  if (리스트명 == null) {
    this.리스트명 = null;
  } else {
    this.리스트명 = new ArrayList<>();
    for (VO명 item : 리스트명) {
      this.리스트명.add(new VO명(item));
    }
  }
}

new VO명(item) 또는 item.clone()으로 깊은 복사하면 내부 원소 참조가 아닌 값을 복사하여 보안이 강화됩니다.

Public 메소드로부터 반환된 Private 배열

VO에서 Public getter 함수를 통해 Private 변수를 리턴하면, 외부에서 값을 수정할 수 있습니다.
Private 변수 객체의 복제본을 만들어서 반환해야 외부 수정을 방지할 수 있습니다.

private 배열 반환 개선

public String[] get배열명() {
  if (this.배열명 == null) {
    return null;
  } else {
    String[] tempData = Arrays.copyOf(배열명, 배열명.length);

    return tempData;
  }
}

새로운 변수에 담아서 리턴해야 보안 취약점으로 탐지되지 않습니다.

private List 반환 개선 (얕은 복사)</mark>

public List<VO명> get리스트명() {
  if (this.리스트명 == null) {
    return null;
  } else {
    List<VO명> tempList = new ArrayList<>(리스트명);
    
    return tempList;
  }
}

오류메시지를 통한 정보노출

try {
  // 예외 발생 가능 코드
} catch (IllegalArgumentException e) {
  LOGGER.error("잘못된 입력: {}", e.getMessage(), e);
  mav.addObject("status", "fail");
  mav.addObject("message", "잘못된 요청입니다."); 
} catch (Exception e) {
  LOGGER.error("서버 내부 오류: {}", e.getMessage(), e);
  mav.addObject("status", "fail");
  mav.addObject("message", "서버 내부 오류입니다.");
}
mav.addObject("result", result);
mav.setViewName("jsonView");

LOGGER로 예외 메시지 출력 시 공격자가 프로그램 내부 구조를 쉽게 파악할 수 있습니다.
개발 완료 후 운영 이관 시 디버깅 코드를 제거하는 것이 좋습니다.

예외 출력 구분

  • e.getMessage() : 예외 메시지만 출력
  • e.toString() : 예와 타입(클래스) + 예외 메시지 출력
  • e.printStackTrace() 또는 e : 예외 타입(클래스) + 예외 메시지 + 전체 스택 트레이스 출력

오류 상황 대응 부재

try {
  // 예외 발생 가능 코드
} catch (Exception e) {

}

위와 같이, catch 문에서 아무런 처리를 하지 않으면 발생하는 보안 취약점입니다.
try catch 문을 지우고 throws Exception 처리하는 것도 방법입니다.

주석문 안에 포함된 시스템 주요정보 (JSP의 HTML 주석)

HTML 주석에 노출된 정보는 공격자가 시스템을 파악하고 공격 계획을 세우는데 도움이 됩니다.

<!-- HTML 주석 -->
HTML 코드

아래처럼, JSP에서는 HTML 주석 대신 Java 주석을 사용하면 개발자 도구에서 보이지 않습니다.

<% /* Java 주석 */ %>

보안 취약점 보완 시 문제 해결

BOM(Byte Order Mark) 문제

CODE-RAY 툴 내에서 코드 수정 시, 파일 인코딩에 BOM이 포함되어 war 빌드 시 아래의 오류가 발생합니다.

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.10.1:compile (default-compile) on project common: Compilation failure: Compilation failure: 
[ERROR] /D:/new_intelliJ/eGovFrameDev-4.1.0-64bit/workspace/genia/src/main/java/com/intermorph/cmmn/imCase/web/IMCaseUserController.java:[1,1] illegal character: '\ufeff'
[ERROR] /D:/new_intelliJ/eGovFrameDev-4.1.0-64bit/workspace/genia/src/main/java/com/intermorph/cmmn/imCase/web/IMCaseUserController.java:[1,10] class, interface, or enum expected
[ERROR] -> [Help 1]
[ERROR]

BOM은 유니코드 텍스트 파일의 인코딩 방식을 표시하는 숨겨진 특수한 문자입니다.
파일 맨 앞에 \ufeff 같은 형식으로 저장되며, Java 컴파일러 등이 BOM을 이해하지 못하면 오류가 발생할 수 있습니다.

BOM 문자 제거 방법
Notepad++ 실행 > 문제 파일 열기 > 인코딩 > UTF-8 선택 > 파일 저장

Leave a comment