세상에는 수많은 해킹 기법들이 있다. 학부생 때 컴퓨터 시스템 개론 과목을 들으면서 attack lab을 하며 buffer overflow를 처음 배우고 사용해 봤을 때의 충격이란... 정말 놀라웠다. 사실상 프로그램에 원하는 명령어를 넣는다는 것은 관리자 권한을 얻을수 있다는 말이고, 즉슨 해당 프로그램의 사용자 정보를 탈취하거나 서버 정보에 악의적인 공격을 해 막대한 손해를 끼칠 수 있다는 말이다.
따라서 코드를 작성할 때 이러한 공격을 방지할 수 있어야 한다. secure coding은 보안 취약점이 없는 코드를 작성한다는 의미이며, 안정적인 서비스를 위해 꼭 필요한 개념이다.
지금까지 SW개발병으로 근무하면서 수많은 보안 취약점들을 보아 왔고, 그 중 대표적인 몇 가지에 대해 어떤 문제가 있는지, spring에서는 어떻게 해결해야 하는지 살펴보고자 한다.
XSS (cross-site scripting)
XSS는 동적 웹 페이지의 입력 폼에 javascript 명령어를 넣는 등의 방식을 통해 해당 페이지에 접근하는 사용자의 브라우저에서 악의적인 스크립트를 실행시키는 공격 방법이다.
크게 Stored XSS와 Reflected XSS가 있다.
먼저 Stored XSS는 서버에 스크립트를 서버에 저장시키는 방법이다. 예를 들어 닉네임이나, 게시글, 댓글과 같이 사용자가 입력하는 부분에 아래와 같은 명령어를 넣어 공격자가 의도한 스크립트를 실행시키는 방식이다.
Reflected XSS는 URL에 스크립트를 넣어 실행하는 방법이다. 예를 들어 query parameter에 스크립트를 삽입해서 공격자가 의도한 스크립트를 실행하는 방법이다.
<!-- Stored XSS -->
<script>alert('XSS');</script>
<img src="" onload="alert('XSS')">
<!-- Reflected XSS -->
http://hyelie.tistory.com/page?number=<script>alert('XSS');</script>
해결 방법
가장 쉬운 해결방법은 모든 입력값에 대해 <, >, ", '와 같은 script에서 사용하는 특수문자를 HTML character entity refernce로 바꾸는 방법으로 바꾸는 것이다.
- < → <
- > → >
- " → "
- ' → '
이 경우, 모든 사용자 입력값에 대해 검증해야 하기 때문에 코드가 난잡해질 수 있으며 사용자가 html을 입력할 수 없다는 단점이 있다. 그러나 난잡한 코드는 servlet filter로 대체할 수 있고, 사용자가 직접 html을 입력하게 하는 대신 개발자가 설정한 특정 태그만 white-list로 열어두던지, 아니면 정해진 format으로만 출력되게 하는 방법이 있을 것이다.
추가로 JSTL의 c:out 태그를 이용해 사용자 입력을 화면에 출력해야 하는 경우, 문자열로 치환하고 출력하는 방법도 있다.
SQL Injection
SQL Injection은 사용자가 입력한 값이 필터링이나 이스케이핑 없이 DB로 들어가는 경우에 할 수 있는 공격 방법이다. 변수가 바로 SQL문에 들어가는 경우 주석처리나 OR 등의 연산자를 통해 원하는 SQL을 실행할 수 있게 된다.
예를 들어, 아래와 같은 로그인 시 사용하는 쿼리가 있다고 하자. 이 때 myID는 사용자가 입력한 아이디라고 생각하자.
SELECT * FROM user WHERE user.id = 'myID' AND user.password = 'myPassword';
별도의 검사 없이 사용자의 입력을 바로 여기에 넣어버리면 문제가 발생할 수 있다 .만약 사용자가 id를 'a' OR '1' = '1'; -- 라고 입력하면 위 쿼리가 아래와 같이 바뀐다.
SELECT * FROM user WHERE user.id = 'a' OR '1' = '1'; -- AND user.password = 'myPassword';
SQL의 문법상 뒤에 들어가는 password 검증 부분이 주석처리되고, 사용자 id가 틀림에도 불구하고 or 문에 의해서 모든 사용자의 정보를 가져올 수 있게 된다. 이렇게 하지 않더라도 세미콜론 이후에 원하는 SQL을 작성하면 모든 DB가 노출되게 된다.
해결 방법
가장 쉬운 해결방법은 prepared statement를 사용하는 것이다. prepared statement가 무엇인지 설명하려면 그것만으로도 포스팅 하나가 나오니, 간단하게만 설명하자면 "SQL 캐싱을 통해 사용자 입력을 순수 문자열로만 치환해서 SQL을 날리는 방법"입니다. SQL Injection을 막아줄 뿐만 아니라 캐싱해두기 때문에 같은 SQL을 실행시킬 때 시간이 단축된다는 장점도 있다.
아래 예시는 자바 문법이다. prepared statement의 사용 방법은 언어마다 다르기 때문에 언어에 맞는 것을 골라 사용하면 된다.
PreparedStatement pstmt =
con.prepareStatement("SELECT * FROM user WHERE user.id = ? AND user.password = ?");
pstmt.setString(1, "myID");
pstmt.setInt(2, "myPassword");
MyBatis와 같은 mapper를 사용할 때에는 변수를 넣고 SQL을 인코딩하는 $ 방식이 아니라, prepared statement를 사용하는 방식과 같은 #를 사용해 변수를 넣어야 한다.
<!-- 이런 방식은 SQL Injection에 취약함 -->
<select id="selectStudent" parameterClass="Student">
SELECT * FROM STUDENTS
WHERE id = $id$ AND name = $name$
</select>
<!-- 이런 방식을 사용해야 함 -->
<select id="selectStudent" parameterClass="Student">
SELECT * FROM STUDENTS
WHERE id = #id# AND name = #name#
</select>
Path Traversal
Path Traversal은 파일 다운로드 시 파일 이름을 이용한다는 것에 착안하여 window의 경우 ../이나, unix의 경우 ..\와 같이 상위 폴더로 움직여 시스템의 중요한 정보를 탈취할 수 있는 공격 기법이다.
https://hyelie.tistory.com/download?filename=../../etc/passwd
해결 방법
가장 쉬운 해결방법은 사용자 입력에 ../, ./, .|, ..|, ..\, .\, ||와 같은 특수문자가 있으면 지워버리거나 접근을 차단하는 방법이다. 단순히 ../를 공백으로 치환해버린다면 ....//와 같이 여러 번 중첩해서 사용할 경우 뚫릴 위험이 있기 때문이다.
String filename = // ... 파일 이름 받아옴
if(filename == null) throw new Exception("파일 이름 오류");
// 해결책 1. 파일 이름이 올바르지 않은 경우 예외 발생
if(filename.contains("../") || filename.contains("./") ||
filename.contains("..\\") || filename.contains(".\\") ||
filename.contains("|")){
throw new Exception("파일 이름 오류");
}
// 해결책 2. 해당 특수문자 공백으로 치환
filename = filename.replaceAll("/", "");
filename = filename.replaceAll("\\", "");
filename = filename.replaceAll(".", "");
// ...file process logic
추가로, 파일이 업로드되는 공간을 정해놓고 사용자 입력으로 생성한 파일 경로가 해당 경로 안에 있어야만 파일 다운로드 로직이 실행되도록 하는 방법도 있겠다.
// 사용자 입력을 바탕으로 파일 생성
File file = new File(BASE_DIRECTORY, userInput);
if(file == null) throw new Exception("파일이 존재하지 않음");
// BASE_DIRECTORY : 정해져 있는 파일 저장 위치 경로
// 해당 파일 경로가 BASE_DIRECTORY에서 시작하지 않는다면 예외 발생
if (file.getCanonicalPath().startsWith(BASE_DIRECTORY)) {
// process
}
else{
throw new Excpetion("파일 경로가 올바르지 않습니다");
}
Upload Attack (Web Shell)
Upload Attack은 첨부파일과 같이 사용자가 파일을 서버로 업로드하는 기능을 악용해 서버에서 실행되는 .jsp나 .php와 같은 스크립트를 업로드하고, 해당 스크립트를 통해 서버측의 권한을 탈취할 수 있다. 여러 방어를 우회하기 위해 a.jsp.jpg와 같이 확장자를 2개 붙이기도 하고, a.jSp와 같이 대소문자를 바꿔넣기도 하고, a.jsp%00.jpg와 같이 null 문자열을 중간에 넣기도 한다.
해결 방법
가장 쉬운 해결방법은 업로드한 파일 이름의 뒤에서부터 검사하고, white-list 방식을 사용하는 것이다. 아래 예제는 스프링 예시이다. 추가로,특수문자가 사용되었는지 정규식을 통해 검사하는 것도 좋을 것이다.
// ... request에서 file을 받아옴
MultipartFile file = request.getFile();
if(file == null) return;
int fileSize = file.getSize();
if(fileSize > MAX_FILE_SIZE) throw new Exception("파일 최대크기 초과");
// file 이름을 소문자로 받아옴
String fileName = file.getOriginalFilename().toLowerCase();
if(fileName == null) throw new Exception("파일 이름 오류");
// 확장자 뒤에서부터 확인하며 허용된 확장자만 업로드함
if(fileName.endsWith(".hwp") || fileName.endsWith(".jpg")){
// file upload logic
}
else{
throw new Exception("허용되지 않는 확장자");
}
정리
지금까지 내가 제일 많이 다루었던 XSS, SQL Injection, path traversal, web shell 이렇게 4개 공격에 대해 요약했다. 요지는 절대 사용자의 입력을 믿지 마라는 것이다. 솔직히 사용자 입력에 대해 검증만 잘 해준다면 시큐어 코딩의 절반은 따라가는 것이라 생각한다.
'CS > Security' 카테고리의 다른 글
[Security] 1:1, group E2EE 암호화 방법 ***TODO (0) | 2022.09.26 |
---|