Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ mvn spring-boot:run
- 관리(`/admin/**`, `/members/**`) 및 회원(`/mypage/**`)·게시판 관리(`/inform/**`) 경로는 GET 포함 전체 메서드에 대해 JWT 토큰 인증이 필요합니다.
- 그 외 POST/PUT/DELETE 변경계 요청도 기본적으로 JWT 토큰 인증이 필요합니다.
- 상세 규칙은 `SecurityConfig.java` 의 `filterChain()` 정의를 참고합니다.
- `HTMLTagFilter`가 래핑한 요청의 `getParameter()`와 `getParameterValues()`는 모두 `<`, `>`, `&`, `"`, `'`를 각각 `&lt;`, `&gt;`, `&amp;`, `&quot;`, `&apos;`로 변환합니다. 반복 파라미터의 순서를 유지하며, 여러 번 읽거나 반환 배열을 수정해도 원본 요청 값은 바뀌지 않습니다. 이미 엔티티 형태인 입력도 동일하게 처리됩니다(예: `&lt;` → `&amp;lt;`).
- 인증 미통과는 401, 인증은 되었으나 권한이 부족한 경우(예: `ROLE_USER` 가 `/admin/**` 접근)에는 403 응답이 반환됩니다.

#### JWT 토큰 인증 방법
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,57 +24,30 @@ public HTMLTagFilterRequestWrapper(HttpServletRequest request) {
super(request);
}

@Override
public String[] getParameterValues(String parameter) {

String[] values = super.getParameterValues(parameter);

if(values==null){
return null;
}

for (int i = 0; i < values.length; i++) {
if (values[i] != null) {
StringBuffer strBuff = new StringBuffer();
for (int j = 0; j < values[i].length(); j++) {
char c = values[i].charAt(j);
switch (c) {
case '<':
strBuff.append("&lt;");
break;
case '>':
strBuff.append("&gt;");
break;
//case '&':
//strBuff.append("&amp;");
//break;
case '"':
strBuff.append("&quot;");
break;
case '\'':
strBuff.append("&apos;");
break;
default:
strBuff.append(c);
break;
}
}
values[i] = strBuff.toString();
} else {
values[i] = null;
}
if (values == null) {
return null;
}

return values;
String[] escapedValues = values.clone();
for (int i = 0; i < escapedValues.length; i++) {
escapedValues[i] = escapeHtml(escapedValues[i]);
}
return escapedValues;
}

@Override
public String getParameter(String parameter) {

String value = super.getParameter(parameter);

if(value==null){
return escapeHtml(super.getParameter(parameter));
}

private String escapeHtml(String value) {
if (value == null) {
return null;
}

StringBuffer strBuff = new StringBuffer();

for (int i = 0; i < value.length(); i++) {
Expand All @@ -100,10 +73,7 @@ public String getParameter(String parameter) {
break;
}
}

value = strBuff.toString();

return value;
return strBuff.toString();
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package egovframework.com.cmm.filter;

import static org.assertj.core.api.Assertions.assertThat;

import java.io.IOException;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.EnumSet;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServer;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;

import jakarta.servlet.DispatcherType;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

class HTMLTagFilterRequestWrapperTest {

@Test
void scalarAndArrayEscapeAllFiveCharacters() {
for (String[] example : new String[][] {
{"<", "&lt;"}, {">", "&gt;"}, {"&", "&amp;"},
{"\"", "&quot;"}, {"'", "&apos;"},
{"<a title=\"A&B\">'한글'</a>", "&lt;a title=&quot;A&amp;B&quot;&gt;&apos;한글&apos;&lt;/a&gt;"},
{"&lt;&#38;", "&amp;lt;&amp;#38;"}, {"", ""}, {"plain 한글", "plain 한글"}}) {
MockHttpServletRequest request = new MockHttpServletRequest();
request.addParameter("value", example[0]);
HTMLTagFilterRequestWrapper wrapper = new HTMLTagFilterRequestWrapper(request);
assertThat(wrapper.getParameter("value")).isEqualTo(example[1]);
assertThat(wrapper.getParameterValues("value")).containsExactly(example[1]);
}
}

@Test
void repeatedReadsPreserveOrderAndUnderlyingRequest() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.addParameter("value", "A&B", "<second>", "");
HTMLTagFilterRequestWrapper wrapper = new HTMLTagFilterRequestWrapper(request);
assertThat(wrapper.getParameterValues("value")).containsExactly("A&amp;B", "&lt;second&gt;", "");
assertThat(wrapper.getParameter("value")).isEqualTo("A&amp;B");
assertThat(wrapper.getParameterValues("value")).containsExactly("A&amp;B", "&lt;second&gt;", "");
assertThat(request.getParameterValues("value")).containsExactly("A&B", "<second>", "");
}

@Test
void callerCannotChangeSubsequentReadsThroughReturnedArray() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.addParameter("value", "A&B");
HTMLTagFilterRequestWrapper wrapper = new HTMLTagFilterRequestWrapper(request);
wrapper.getParameterValues("value")[0] = "changed";
assertThat(wrapper.getParameter("value")).isEqualTo("A&amp;B");
assertThat(wrapper.getParameterValues("value")).containsExactly("A&amp;B");
}

@Test
void missingEmptyAndNullValuesRetainTheirShape() {
MockHttpServletRequest request = new MockHttpServletRequest();
HTMLTagFilterRequestWrapper wrapper = new HTMLTagFilterRequestWrapper(request);
assertThat(wrapper.getParameter("missing")).isNull();
assertThat(wrapper.getParameterValues("missing")).isNull();
request.setParameter("empty", new String[0]);
assertThat(wrapper.getParameterValues("empty")).isEmpty();
request.setParameter("nullable", new String[] {null, "", "&"});
assertThat(wrapper.getParameter("nullable")).isNull();
assertThat(wrapper.getParameterValues("nullable")).containsExactly(null, "", "&amp;");
}

@Test
void parameterMapStillDelegatesToTheUnderlyingRequest() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.addParameter("value", "A&B", "<second>");
HTMLTagFilterRequestWrapper wrapper = new HTMLTagFilterRequestWrapper(request);
wrapper.getParameterValues("value");
assertThat(wrapper.getParameterMap().get("value"))
.isSameAs(request.getParameterMap().get("value"))
.containsExactly("A&B", "<second>");
}

@Test
void httpFormReadsRemainStableThroughEmbeddedTomcat(@TempDir Path baseDirectory) throws Exception {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(0);
factory.setAddress(java.net.InetAddress.getLoopbackAddress());
factory.setBaseDirectory(baseDirectory.toFile());
WebServer server = factory.getWebServer(context -> {
context.addFilter("htmlTagFilter", new HTMLTagFilter())
.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*");
context.addServlet("form", new HttpServlet() {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
request.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType("text/plain;charset=UTF-8");
String first = request.getParameter("value");
String[] values = request.getParameterValues("value");
values[0] = "caller mutation";
response.getWriter().print(first + "\n"
+ String.join("|", request.getParameterValues("value")) + "\n"
+ request.getParameter("value") + "\n"
+ request.getParameter("missing"));
}
}).addMapping("/form");
});
try {
server.start();
String raw = "<>&\"' 한글😀 &lt;";
String escaped = "&lt;&gt;&amp;&quot;&apos; 한글😀 &amp;lt;";
String form = "value=" + URLEncoder.encode(raw, StandardCharsets.UTF_8)
+ "&value=" + URLEncoder.encode("A&B", StandardCharsets.UTF_8) + "&value=";
HttpRequest request = HttpRequest.newBuilder(
new URI("http", null, java.net.InetAddress.getLoopbackAddress().getHostAddress(),
server.getPort(), "/form", null, null))
.timeout(java.time.Duration.ofSeconds(10))
.header("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8")
.POST(HttpRequest.BodyPublishers.ofString(form, StandardCharsets.UTF_8)).build();
HttpResponse<String> response = HttpClient.newHttpClient().send(request,
HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
assertThat(response.statusCode()).isEqualTo(200);
assertThat(response.body()).isEqualTo(escaped + "\n" + escaped + "|A&amp;B|\n" + escaped + "\nnull");
} finally {
server.stop();
}
}

@Test
void filterDeliversEscapedValuesToTheServletChain() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/form");
request.addParameter("value", "A&B", "<second>");
new HTMLTagFilter().doFilter(request, new MockHttpServletResponse(), (filtered, response) -> {
HttpServletRequest downstream = (HttpServletRequest) filtered;
assertThat(downstream.getParameter("value")).isEqualTo("A&amp;B");
assertThat(downstream.getParameterValues("value")).containsExactly("A&amp;B", "&lt;second&gt;");
assertThat(downstream.getParameter("value")).isEqualTo("A&amp;B");
});
}
}