Skip to content

Commit fbb515a

Browse files
authored
Merge pull request #515 from TaskFlow-CLAP/CLAP-372
CLAP-372 XSS 공격 방지 로직 구현
2 parents 1ade25a + ac9a567 commit fbb515a

File tree

9 files changed

+223
-0
lines changed

9 files changed

+223
-0
lines changed

build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ dependencies {
107107
implementation 'org.apache.tika:tika-core:2.9.0'
108108
implementation 'org.apache.tika:tika-parsers:2.9.0'
109109

110+
// Jsoup
111+
implementation 'org.jsoup:jsoup:1.17.1'
110112
}
111113

112114
tasks.named('test') {

src/main/java/clap/server/adapter/inbound/security/SecurityConfig.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,19 @@ public SecurityFilterChain defaultFilterChain(HttpSecurity http) throws Exceptio
6363

6464
private HttpSecurity defaultSecurity(HttpSecurity http) throws Exception {
6565
return http
66+
.headers(headers -> headers
67+
.contentSecurityPolicy(csp ->
68+
csp.policyDirectives(
69+
"default-src 'self'; " +
70+
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; " +
71+
"style-src 'self' 'unsafe-inline'; " +
72+
"img-src 'self' data: https:; " +
73+
"font-src 'self' data: https:; " +
74+
"object-src 'none'; " +
75+
"base-uri 'self';"
76+
)
77+
)
78+
)
6679
.httpBasic(AbstractHttpConfigurer::disable)
6780
.csrf(AbstractHttpConfigurer::disable)
6881
.sessionManagement(
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package clap.server.adapter.inbound.web.xss;
2+
3+
import clap.server.common.annotation.architecture.WebAdapter;
4+
import clap.server.common.annotation.swagger.DevelopOnlyApi;
5+
import io.swagger.v3.oas.annotations.Operation;
6+
import io.swagger.v3.oas.annotations.tags.Tag;
7+
import lombok.extern.slf4j.Slf4j;
8+
import org.springframework.http.ResponseEntity;
9+
import org.springframework.web.bind.annotation.*;
10+
11+
@Slf4j
12+
@WebAdapter
13+
@RequestMapping("/api/xss-test")
14+
@Tag(name = "xss 공격 테스트 API", description = "아래와 같은 페이로드들에 대해 테스트합니다.\n" +
15+
"1. 기본적인 스크립트 삽입: `<script>alert('xss')</script>`\n" +
16+
"2. 이미지 태그를 이용한 XSS: `<img src=x onerror=alert('xss')>`\n" +
17+
"3. JavaScript 프로토콜: `javascript:alert('xss')`\n" +
18+
"4. HTML 이벤트 핸들러:` <div onmouseover=\"alert('xss')\">hover me</div>`\n" +
19+
"5. SVG를 이용한 XSS: `<svg><script>alert('xss')</script></svg>`\n" +
20+
"6. HTML5 태그를 이용한 XSS: `<video><source onerror=\"alert('xss')\">`")
21+
public class XssTestController {
22+
23+
@GetMapping
24+
@DevelopOnlyApi
25+
@Operation(summary = "단일 파라미터 test")
26+
public ResponseEntity<String> testGetXss(@RequestParam String input) {
27+
log.info("Received GET input: {}", input);
28+
return ResponseEntity.ok("Processed GET input: " + input);
29+
}
30+
31+
@PostMapping
32+
@DevelopOnlyApi
33+
@Operation(summary = "dto test")
34+
public ResponseEntity<XssTestResponse> testPostXss(@RequestBody XssTestRequest request) {
35+
log.info("Received POST input: {}", request);
36+
return ResponseEntity.ok(new XssTestResponse(request.content()));
37+
}
38+
39+
@GetMapping("/multi-params")
40+
@Operation(summary = "다중 파라미터 테스트")
41+
public ResponseEntity<String> testMultiParamXss(@RequestParam(value = "inputs", required = false) String[] inputs) {
42+
if (inputs == null || inputs.length == 0) {
43+
return ResponseEntity.badRequest().body("No inputs provided");
44+
}
45+
46+
StringBuilder response = new StringBuilder("Processed inputs:\n");
47+
for (int i = 0; i < inputs.length; i++) {
48+
log.info("Received input {}: {}", i, inputs[i]);
49+
response.append("Input ").append(i).append(": ").append(inputs[i]).append("\n");
50+
}
51+
52+
return ResponseEntity.ok(response.toString());
53+
}
54+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package clap.server.adapter.inbound.web.xss;
2+
3+
import jakarta.validation.constraints.NotNull;
4+
5+
public record XssTestRequest(
6+
@NotNull
7+
String content
8+
) {}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package clap.server.adapter.inbound.web.xss;
2+
3+
public record XssTestResponse(
4+
String sanitizedContent
5+
) {}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package clap.server.adapter.inbound.xss;
2+
3+
import jakarta.servlet.*;
4+
import jakarta.servlet.http.HttpServletRequest;
5+
import org.springframework.stereotype.Component;
6+
7+
import java.io.IOException;
8+
9+
@Component
10+
public class XssPreventionFilter implements Filter {
11+
12+
@Override
13+
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
14+
throws IOException, ServletException {
15+
if (request instanceof HttpServletRequest) {
16+
HttpServletRequest wrappedRequest = new XssRequestWrapper((HttpServletRequest) request);
17+
chain.doFilter(wrappedRequest, response);
18+
} else {
19+
chain.doFilter(request, response);
20+
}
21+
}
22+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package clap.server.adapter.inbound.xss;
2+
3+
import jakarta.servlet.http.HttpServletRequest;
4+
import jakarta.servlet.http.HttpServletRequestWrapper;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.jsoup.Jsoup;
7+
import org.jsoup.safety.Safelist;
8+
9+
import java.util.Arrays;
10+
import java.util.Optional;
11+
12+
@Slf4j
13+
public class XssRequestWrapper extends HttpServletRequestWrapper {
14+
15+
public XssRequestWrapper(HttpServletRequest request) {
16+
super(request);
17+
}
18+
19+
@Override
20+
public String[] getParameterValues(String parameter) {
21+
return Optional.ofNullable(super.getParameterValues(parameter))
22+
.map(values -> Arrays.stream(values)
23+
.map(this::sanitize)
24+
.toArray(String[]::new))
25+
.orElse(null);
26+
}
27+
28+
@Override
29+
public String getParameter(String parameter) {
30+
String value = super.getParameter(parameter);
31+
String sanitizedValue = Optional.ofNullable(value)
32+
.map(this::sanitize)
33+
.orElse(null);
34+
log.info("Original parameter [{}]: {}", parameter, value);
35+
log.info("Sanitized parameter [{}]: {}", parameter, sanitizedValue);
36+
return sanitizedValue;
37+
}
38+
39+
@Override
40+
public String getHeader(String name) {
41+
return Optional.ofNullable(super.getHeader(name))
42+
.map(this::sanitize)
43+
.orElse(null);
44+
}
45+
46+
47+
public String sanitize(String value) {
48+
if (value == null) {
49+
return null;
50+
}
51+
if (value.toLowerCase().startsWith("javascript:")) {
52+
return "";
53+
}
54+
return Jsoup.clean(value, Safelist.basic());
55+
}
56+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package clap.server.config.jackson;
2+
3+
import com.fasterxml.jackson.core.JsonParser;
4+
import com.fasterxml.jackson.databind.DeserializationContext;
5+
import com.fasterxml.jackson.databind.JsonDeserializer;
6+
import com.fasterxml.jackson.databind.ObjectMapper;
7+
import com.fasterxml.jackson.databind.module.SimpleModule;
8+
import lombok.extern.slf4j.Slf4j;
9+
import org.jsoup.Jsoup;
10+
import org.jsoup.safety.Safelist;
11+
import org.springframework.context.annotation.Bean;
12+
import org.springframework.context.annotation.Configuration;
13+
14+
import java.io.IOException;
15+
16+
// XSS 방지를 위한 Jackson 설정
17+
@Slf4j
18+
@Configuration
19+
public class JacksonConfig {
20+
21+
@Bean
22+
public ObjectMapper objectMapper() {
23+
ObjectMapper mapper = new ObjectMapper();
24+
SimpleModule module = new SimpleModule();
25+
module.addDeserializer(String.class, new JsonHtmlXssDeserializer());
26+
mapper.registerModule(module);
27+
return mapper;
28+
}
29+
30+
public static class JsonHtmlXssDeserializer extends JsonDeserializer<String> {
31+
@Override
32+
public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
33+
String value = p.getText();
34+
if (value == null) {
35+
return null;
36+
}
37+
if (value.toLowerCase().startsWith("javascript:")) {
38+
return "";
39+
}
40+
return Jsoup.clean(value, Safelist.basic());
41+
}
42+
}
43+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package clap.server.config.web;
2+
3+
import clap.server.adapter.inbound.xss.XssPreventionFilter;
4+
import org.springframework.boot.web.servlet.FilterRegistrationBean;
5+
import org.springframework.context.annotation.Bean;
6+
import org.springframework.context.annotation.Configuration;
7+
import org.springframework.core.Ordered;
8+
9+
@Configuration
10+
public class WebConfig {
11+
12+
@Bean
13+
public FilterRegistrationBean<XssPreventionFilter> xssPreventionFilterRegistrationBean() {
14+
FilterRegistrationBean<XssPreventionFilter> registrationBean = new FilterRegistrationBean<>();
15+
registrationBean.setFilter(new XssPreventionFilter());
16+
registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
17+
registrationBean.addUrlPatterns("/*");
18+
return registrationBean;
19+
}
20+
}

0 commit comments

Comments
 (0)