외부 입력 데이터를 안전하게 검증하고 표현하여 SQL Injection, XSS, Path Traversal 등의 공격을 방어하는 가이드입니다.
DBMS와 연동된 응용프로그램에서 입력된 데이터에 대한 유효성 검증을 하지 않을 경우 공격자가 SQL 문을 삽입하여 정보를 열람하거나 조작할 수 있는 보안 약점입니다.
/* 취약한 코드 (Java JDBC API) */
String gubun = request.getParameter("gubun");
// 외부 입력값을 문자열로 바로 사용
String sql = "SELECT * FROM board WHERE b_gubun = '" + gubun + "'";
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery(sql); // 공격 가능한 구문 실행
/* 안전한 코드 (Java JDBC API) */
String gubun = request.getParameter("gubun");
String sql = "SELECT * FROM board WHERE b_gubun = ?"; // Placeholder 사용
PreparedStatement pstmt = con.prepareStatement(sql);
pstmt.setString(1, gubun); // 입력값을 문자열로 취급하여 쿼리 구조 변경 방지
ResultSet rs = pstmt.executeQuery();
공격자가 소프트웨어의 의도된 동작을 변경하도록 임의 코드를 삽입하여 비정상적으로 동작하게 합니다.
/* 취약한 코드 (Java) */
@RequestMapping(value = "/execute", method = RequestMethod.GET)
public String execute(@RequestParam("src") String src)
throws ScriptException {
ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
ScriptEngine scriptEngine = scriptEngineManager.getEngineByName("javascript");
// 외부 입력값인 src를 javascript eval 함수로 실행하고 있어 안전하지 않다.
String retValue = (String)scriptEngine.eval(src);
return retValue;
}
/* 안전한 코드 (Java) */
@RequestMapping(value = "/execute", method = RequestMethod.GET)
public String execute(@RequestParam("src") String src) throws ScriptException {
// 1. 정규식을 이용하여 특수문자 입력 시 예외를 발생시킨다.
if (src.matches("[\\w\\s]*") == false) {
throw new IllegalArgumentException();
}
ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
ScriptEngine scriptEngine = scriptEngineManager.getEngineByName("javascript");
String retValue = (String)scriptEngine.eval(src);
return retValue;
}
검증되지 않은 외부 입력값으로 파일 경로 또는 시스템 자원 식별(포트, IP 등)을 허용하여 시스템이 보호하는 자원에 임의로 접근할 수 있는 보안 약점입니다.
/* 취약한 코드 (Java) */
String fileName = request.getParameter("P");
// ... (생략)
// 외부 입력값이 검증 없이 파일처리에 수행
fis = new FileInputStream("C:/datas/" + fileName); // 임의 파일 접근 가능
/* 안전한 코드 (Java) */
String fileName = request.getParameter("P");
// 외부 입력받은 값에서 경로순회 문자열 (/, \, .. 등)을 제거
filename = filename.replaceAll("..", "")
.replaceAll("/", "")
.replaceAll("\\\\", "");
fis = new FileInputStream("C:/datas/" + fileName);
웹사이트에 악성 코드를 삽입하는 공격 방법으로, 공격자는 대상 웹 응용프로그램의 결함을 이용해 악성 코드(JavaScript)를 사용자에게 보냅니다.
/* 취약한 코드 (Java JSP) */
String keyword = request.getParameter("keyword");
// 검증 없이 사용자 입력값을 HTML에 출력
out.println("검색어 : " + keyword);
// keyword에 <script>...</script> 입력 시 XSS 발생
/* 안전한 코드 (Java JSP) */
String keyword = request.getParameter("keyword");
// 1. 입력값에 대하여 스크립트 공격가능성이 있는 문자열을 치환한다.
keyword = keyword.replaceAll("<", "<").replaceAll(">", ">");
// 2. JSP에서 출력값에 JSTL c:out을 사용한다.
// <c:out value="${keyword}"/>
적절한 검증 절차를 거치지 않은 사용자 입력값이 운영체제 명령어의 일부 또는 전부로 구성되어 실행되는 경우 의도하지 않은 시스템 명령어가 실행될 수 있습니다.
/* 취약한 코드 (Java) */
// 해당 프로그램에서 실행할 프로그램을 제한하고 있지 않아 파라미터로 전달되는 모든 프로그램이 실행될 수 있다.
String cmd = args[0]; // 외부 입력
Process ps = null;
try {
ps = Runtime.getRuntime().exec(cmd);
} catch (IOException e) { ... }
/* 안전한 코드 (Java) */
List<String> allowedCommands = new ArrayList<String>();
allowedCommands.add("notepad"); allowedCommands.add("calc");
String cmd = args[0];
// 입력받은 명령어가 허용 목록에 포함되는지 검사
if (!allowedCommands.contains(cmd)) {
System.err.println("허용되지 않은 명령어입니다.");
return;
}
Process ps = Runtime.getRuntime().exec(cmd);
서버 측에서 실행 가능한 스크립트 파일(`.jsp`, `.php` 등)이 업로드가능하고, 이 파일을 공격자가 웹으로 직접 실행시킬 수 있는 경우 시스템 내부 명령어를 실행하거나 외부와 연결해 시스템을 제어할 수 있는 보안 약점입니다.
/* 취약한 코드 (Java) */
// 업로드할 파일에 대한 유효성 검사를 하지 않으면, 위험한 유형의 파일을 공격자가 업로드하거나 전송할 수 있다.
MultipartRequest multi = new MultipartRequest(request, savePath, sizeLimit, "euc-kr", new DefaultFileRenamePolicy());
String fileName = multi.getFilesystemName("filename");
// 업로드 파일명을 검증 없이 DB에 저장
sql = "INSERT INTO board(...) values (..., ?, ?)";
// -> 공격자가 .jsp 파일 등을 업로드 가능
/* 안전한 코드 (Java) */
String fileName = multi.getFilesystemName("filename");
if (fileName != null) {
// 1. 파일 확장자 추출 및 소문자 변환
String fileExt = fileName.substring(fileName.lastIndexOf(".")+1).toLowerCase();
// 2. 화이트 리스트 방식으로 허용되는 확장자로 제한
if (!"gif".equals(fileExt) && !"jpg".equals(fileExt) && !"png".equals(fileExt)) {
alertMessage("업로드 불가능한 파일입니다.");
return;
}
}
// 안전한 파일명만 DB에 저장
사용자 입력값을 외부 사이트 주소로 사용하여 자동으로 연결하는 서버 프로그램은 피싱 공격(Open Redirect)에 노출되는 취약점을 가집니다.
/* 취약한 코드 (Java) */
String rd = request.getParameter("redirect");
// ... (권한 검증 후)
if ("0".equals(rs.getString(1)) && "01AD".equals(bn)) {
// 외부 입력값을 검증 없이 바로 Redirect에 사용
response.sendRedirect(rd);
return;
}
/* 안전한 코드 (Java) */
// 이동 할 수 있는 URL 범위를 제한하여 피싱 사이트 등으로 이동하지 못하도록 한다.
String allowedUrl[] = { "/main.do", "/login.jsp", "list.do" };
String rdIndex = request.getParameter("redirect");
String rd;
try {
// 입력값을 인덱스로 사용하여 화이트리스트에서 선택
rd = allowedUrl[Integer.parseInt(rdIndex)];
} catch(Exception e) {
return "잘못된 접근입니다.";
}
// ... (권한 검증 후)
response.sendRedirect(rd);
취약한 XML 파서가 외부 엔티티(DTD) 처리를 허용하도록 설정된 경우, 공격자가 삽입한 공격 구문이 동작되어 서버 파일 접근, 정보 노출 등이 발생할 수 있습니다.
/* 취약한 코드 (Java JAXB) */
// 외부 엔티티 참조 제한 설정 없이 document를 생성하여 안전하지 않다.
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document document = db.parse(receivedXml);
// 외부 엔티티로 만들어진 document를 이용하여 마샬링을 수행하여 안전하지 않다.
Student employee = (Student) jaxbUnmarshaller.unmarshal( document );
/* 안전한 코드 (Java JAXP) */
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
// XML 파서가 doctype을 정의하지 못하도록 설정한다.
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
// 외부 일반 엔티티를 포함하지 않도록 설정한다.
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
// 외부 DTD 비활성화한다.
dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
DocumentBuilder db = dbf.newDocumentBuilder();
Document document = db.parse(receivedXml);
검증되지 않은 외부 입력 값이 XQuery 또는 XPath 쿼리문을 생성하는 문자열로 사용되어, 허가되지 않은 데이터를 열람하거나 인증절차를 우회할 수 있습니다.
/* 취약한 코드 (Java) */
String name = props.getProperty("name");
// 외부 입력 값에 의해 쿼리 구조가 변경되어 안전하지 않다.
String es = "doc('users.xml')/userlist/user[uname='"+name+"']";
XQPreparedExpression expr = conn.prepareExpression(es);
// name에 'something' or '1'='1 입력 시 모든 파일 값 출력 가능
/* 안전한 코드 (Java) */
String name = props.getProperty("name");
// 1. 바인딩 변수 ($xname)를 사용
String es = "doc('users.xml')/userlist/user[uname='$xname']";
XQPreparedExpression expr = conn.prepareExpression(es);
// 2. bindString 함수로 입력값을 바인딩하여 쿼리 구조 변경을 방지
expr.bindString(new QName("xname"), name, null);
XQResultSequence result = expr.executeQuery();
외부 입력값을 LDAP 조회를 위한 필터 생성에 사용될 경우, LDAP 쿼리문의 내용을 마음대로 변경할 수 있으며 권한 상승 등의 공격에 노출될 수 있습니다.
/* 취약한 코드 (Java) */
// userSN과 userPassword 값에 LDAP 필터를 조작할 수 있는 공격 문자열에 대한 검증이 없어 안전하지 않다.
String filter = "(&(sn=" + userSN + ")(userPassword=" + userPassword + "))";
NamingEnumeration> results = dctx.search(base, filter, sc);
// userSN과 userPassword에 *와 같은 문자가 전달되면 권한 상승 가능
/* 안전한 코드 (Java) */
// userSN과 userPassword 값에서 LDAP 필터를 조작할 수 있는 문자열을 제거
if (!userSN.matches("[\\w\\s]*") || !userPassword.matches("[\\w]*")) {
throw new IllegalArgumentException("Invalid input");
}
String filter = "(&(sn=" + userSN + ")(userPassword=" + userPassword + "))";
NamingEnumeration> results = dctx.search(base, filter, sc);
특정 웹사이트에 대해 사용자가 인지하지 못한 상황에서 공격자가 의도한 행위(수정, 삭제, 등록 등)를 요청하게 하는 공격입니다.
/* 취약한 코드 (Java) */
// 토큰 검증 없이 요청 처리
if ("POST".equalsIgnoreCase(request.getMethod())) {
// 상태 변경 로직 실행
}
// -> POST 방식이더라도 CSRF 토큰 검증 없이는 취약
/* 안전한 코드 (Java) */
String pToken = request.getParameter("param_csrf_token");
String sToken = (String)session.getAttribute("SESSION_CSRF_TOKEN");
// 요청 파라미터와 세션에 저장된 토큰을 비교하여 일치하는 경우에만 요청을 처리
if (pToken != null && pToken.equals(sToken)) {
// 정상 처리
} else {
// 토큰이 없거나 값이 일치하지 않는 경우 -> 오류 메시지 출력
}
적절한 검증 절차를 거치지 않은 사용자 입력 값을 서버 간의 요청에 사용하여 악의적인 행위가 발생할 수 있는 보안 약점입니다.
/* 취약한 코드 (Java) */
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 사용자 입력값(url)을 검증없이 사용하여 안전하지 않다.
URL url = new URL(req.getParameter("url"));
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
// -> url에 내부 IP나 file:// 공격 가능
/* 안전한 코드 (Java) */
// URL 목록을 Map 객체에 정의하고 키 값으로 입력받아 참조
private Map<String, URL> urlMap;
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 사용자에게 urlMap의 key를 입력받아 urlMap에서 URL값을 참조한다.
URL url = urlMap.get(req.getParameter("url"));
// urlMap에서 참조한 값으로 Connection을 만들어 접속한다.
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
}
HTTP 요청에 들어 있는 파라미터가 HTTP 응답헤더에 포함되어 사용자에게 다시 전달될 때, 입력값에 개행문자(`CR`, `LF`)가 존재하면 HTTP 응답이 2개 이상으로 분리될 수 있습니다.
/* 취약한 코드 (Java) */
String lastLogin = request.getParameter("last_login");
// ...
// 쿠키는 Set-Cookie 응답헤더로 전달되므로 개행문자열 포함 여부 검증이 필요
Cookie c = new Cookie("LASTLOGIN", lastLogin);
response.addCookie(c);
// -> lastLogin에 개행문자(\r\n) 삽입 시 응답 분할 가능
/* 안전한 코드 (Java) */
String lastLogin = request.getParameter("last_login");
// ...
// 외부 입력값에서 개행문자(\r\n)를 제거한 후 쿠키의 값으로 설정
lastLogin = lastLogin.replaceAll("[\\r\\n]", "");
Cookie c = new Cookie("LASTLOGIN", lastLogin);
response.addCookie(c);
정수형 변수가 저장할 수 있는 범위를 넘어서 아주 작은 수이거나 음수가 되어 프로그램이 예기치 않게 동작할 수 있습니다.
/* 취약한 코드 (Java) */
String tmp = request.getParameter("slf_msg_param_num");
// ...
// 외부 입력값을 정수형으로 사용할 때 입력값의 크기를 검증하지 않고 사용
int param_ct = Integer.parseInt(tmp);
// param_ct가 오버플로우되어 음수가 될 경우, 배열의 크기가 음수로 할당됨
String[] strArr = new String[param_ct];
/* 안전한 코드 (Java) */
try {
int param_ct = Integer.parseInt(tmp);
// 외부 입력값을 정수형으로 사용할 때 입력값의 크기를 검증하고 사용
if (param_ct < 0) {
throw new Exception();
}
String[] strArr = new String[param_ct];
} catch(Exception e) {
msg_str = "잘못된 입력(접근) 입니다.";
}
응용 프로그램이 외부 입력값에 대한 신뢰를 전제로 보호 메커니즘을 사용하는 경우 공격자가 입력값을 조작하여 보호 메커니즘을 우회할 수 있습니다.
/* 취약한 코드 (Java) */
// 서버가 보유한 가격 정보를 사용자 화면에서 받아서 처리
price = request.getParameter("price");
quantity = request.getParameter("quantity");
total = Integer.parseInt(quantity) * Float.parseFloat(price);
// -> 공격자가 price를 조작하여 결제 금액 변경 가능
/* 안전한 코드 (Java) */
item = request.getParameter("item");
// 가격이 아니라 item 항목을 가져와서 서버가 보유하고 있는 가격정보를 이용하여 전체 가격을 계산
price = productService.getPrice(item);
quantity = request.getParameter("quantity");
total = Integer.parseInt(quantity) * price;
할당된 메모리 버퍼의 범위를 넘어선 위치에 자료를 읽거나 써서 프로그램 오동작 또는 악의적인 코드 실행 권한을 획득하게 됩니다. *Java는 언어 특성상 해당 취약점의 직접적인 예시를 제공하지 않습니다.
/* Java는 가비지 컬렉션 및 경계 검사로 인해 메모리 버퍼 오버플로우를 자동으로 방지합니다. */
// (원본 HTML의 C/C++ 코드는 제외했습니다.)
/* Java는 가비지 컬렉션 및 경계 검사로 인해 메모리 버퍼 오버플로우를 자동으로 방지합니다. */
// (원본 HTML의 C/C++ 코드는 제외했습니다.)
외부로부터 입력된 값을 검증하지 않고 입·출력 함수의 포맷 문자열로 그대로 사용하는 경우, 메모리 내용 읽기/쓰기 또는 임의 코드 실행을 유발할 수 있습니다.
/* 취약한 코드 (Java) */
public static void main(String[] args) {
// 외부 입력값인 args[0]이 포맷 문자열 출력에 값으로 사용됨
System.out.printf( args[0] + " did not match! HINT: It was issued on %1$terd
of some month", validate);
}
/* 안전한 코드 (Java) */
public static void main(String[] args) {
// 1. 포맷 문자열로 상수를 사용
System.out.printf("%s did not match! HINT: It was issued on %2$terd of some
month", args[0], validate);
}