Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLAP-372 XSS 공격 방지 로직 구현 #515

Merged
merged 8 commits into from
Feb 13, 2025
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;
}
}