Skip to content
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ dependencies {
implementation 'org.apache.tika:tika-core:2.9.0'
implementation 'org.apache.tika:tika-parsers:2.9.0'

// Jsoup
implementation 'org.jsoup:jsoup:1.17.1'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,19 @@ public SecurityFilterChain defaultFilterChain(HttpSecurity http) throws Exceptio

private HttpSecurity defaultSecurity(HttpSecurity http) throws Exception {
return http
.headers(headers -> headers
.contentSecurityPolicy(csp ->
csp.policyDirectives(
"default-src 'self'; " +
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data: https:; " +
"font-src 'self' data: https:; " +
"object-src 'none'; " +
"base-uri 'self';"
)
)
)
.httpBasic(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package clap.server.adapter.inbound.web.xss;

import clap.server.common.annotation.architecture.WebAdapter;
import clap.server.common.annotation.swagger.DevelopOnlyApi;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@Slf4j
@WebAdapter
@RequestMapping("/api/xss-test")
@Tag(name = "xss 공격 테스트 API", description = "아래와 같은 페이로드들에 대해 테스트합니다.\n" +
"1. 기본적인 스크립트 삽입: `<script>alert('xss')</script>`\n" +
"2. 이미지 태그를 이용한 XSS: `<img src=x onerror=alert('xss')>`\n" +
"3. JavaScript 프로토콜: `javascript:alert('xss')`\n" +
"4. HTML 이벤트 핸들러:` <div onmouseover=\"alert('xss')\">hover me</div>`\n" +
"5. SVG를 이용한 XSS: `<svg><script>alert('xss')</script></svg>`\n" +
"6. HTML5 태그를 이용한 XSS: `<video><source onerror=\"alert('xss')\">`")
public class XssTestController {

@GetMapping
@DevelopOnlyApi
@Operation(summary = "단일 파라미터 test")
public ResponseEntity<String> testGetXss(@RequestParam String input) {
log.info("Received GET input: {}", input);
return ResponseEntity.ok("Processed GET input: " + input);
}

@PostMapping
@DevelopOnlyApi
@Operation(summary = "dto test")
public ResponseEntity<XssTestResponse> testPostXss(@RequestBody XssTestRequest request) {
log.info("Received POST input: {}", request);
return ResponseEntity.ok(new XssTestResponse(request.content()));
}

@GetMapping("/multi-params")
@Operation(summary = "다중 파라미터 테스트")
public ResponseEntity<String> testMultiParamXss(@RequestParam(value = "inputs", required = false) String[] inputs) {
if (inputs == null || inputs.length == 0) {
return ResponseEntity.badRequest().body("No inputs provided");
}

StringBuilder response = new StringBuilder("Processed inputs:\n");
for (int i = 0; i < inputs.length; i++) {
log.info("Received input {}: {}", i, inputs[i]);
response.append("Input ").append(i).append(": ").append(inputs[i]).append("\n");
}

return ResponseEntity.ok(response.toString());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package clap.server.adapter.inbound.web.xss;

import jakarta.validation.constraints.NotNull;

public record XssTestRequest(
@NotNull
String content
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package clap.server.adapter.inbound.web.xss;

public record XssTestResponse(
String sanitizedContent
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package clap.server.adapter.inbound.xss;

import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class XssPreventionFilter implements Filter {

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (request instanceof HttpServletRequest) {
HttpServletRequest wrappedRequest = new XssRequestWrapper((HttpServletRequest) request);
chain.doFilter(wrappedRequest, response);
} else {
chain.doFilter(request, response);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package clap.server.adapter.inbound.xss;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.safety.Safelist;

import java.util.Arrays;
import java.util.Optional;

@Slf4j
public class XssRequestWrapper extends HttpServletRequestWrapper {

public XssRequestWrapper(HttpServletRequest request) {
super(request);
}

@Override
public String[] getParameterValues(String parameter) {
return Optional.ofNullable(super.getParameterValues(parameter))
.map(values -> Arrays.stream(values)
.map(this::sanitize)
.toArray(String[]::new))
.orElse(null);
}

@Override
public String getParameter(String parameter) {
String value = super.getParameter(parameter);
String sanitizedValue = Optional.ofNullable(value)
.map(this::sanitize)
.orElse(null);
log.info("Original parameter [{}]: {}", parameter, value);
log.info("Sanitized parameter [{}]: {}", parameter, sanitizedValue);
return sanitizedValue;
}

@Override
public String getHeader(String name) {
return Optional.ofNullable(super.getHeader(name))
.map(this::sanitize)
.orElse(null);
}


public String sanitize(String value) {
if (value == null) {
return null;
}
if (value.toLowerCase().startsWith("javascript:")) {
return "";
}
return Jsoup.clean(value, Safelist.basic());
}
}
43 changes: 43 additions & 0 deletions src/main/java/clap/server/config/jackson/JacksonConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package clap.server.config.jackson;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.safety.Safelist;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.IOException;

// XSS 방지를 위한 Jackson 설정
@Slf4j
@Configuration
public class JacksonConfig {

@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(String.class, new JsonHtmlXssDeserializer());
mapper.registerModule(module);
return mapper;
}

public static class JsonHtmlXssDeserializer extends JsonDeserializer<String> {
@Override
public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
String value = p.getText();
if (value == null) {
return null;
}
if (value.toLowerCase().startsWith("javascript:")) {
return "";
}
return Jsoup.clean(value, Safelist.basic());
}
}
}
20 changes: 20 additions & 0 deletions src/main/java/clap/server/config/web/WebConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package clap.server.config.web;

import clap.server.adapter.inbound.xss.XssPreventionFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;

@Configuration
public class WebConfig {

@Bean
public FilterRegistrationBean<XssPreventionFilter> xssPreventionFilterRegistrationBean() {
FilterRegistrationBean<XssPreventionFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new XssPreventionFilter());
registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
registrationBean.addUrlPatterns("/*");
return registrationBean;
}
}