package utils; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.Test; class FindXSSTest { // search — script tag detection @Test void search_detectsScriptTagWithAlert() { assertTrue(FindXSS.search("")); } @Test void search_scriptTagWithoutAlertNotDetected() { assertFalse(FindXSS.search("")); } @Test void search_cleanHtmlNotDetected() { assertFalse(FindXSS.search("

Hello World

")); } @Test void search_emptyStringNotDetected() { assertFalse(FindXSS.search("")); } @Test void search_plainTextNotDetected() { assertFalse(FindXSS.search("just some text")); } // search — event handler detection @Test void search_detectsOnclickAlert() { assertTrue(FindXSS.search("
click
")); } @Test void search_detectsOnerrorAlert() { assertTrue(FindXSS.search("")); } @Test void search_detectsOnmouseoverAlert() { assertTrue(FindXSS.search("hover")); } @Test void search_detectsOnpointeroverAlert() { assertTrue(FindXSS.search("
MOVE HERE
")); } @Test void search_detectsOnpointerenterAlert() { assertTrue(FindXSS.search("
hover
")); } @Test void search_detectsOnanimationstartAlert() { assertTrue(FindXSS.search("
animation
")); } @Test void search_detectsOntransitionendAlert() { assertTrue(FindXSS.search("
transition
")); } @Test void search_detectsOnauxclickAlert() { assertTrue(FindXSS.search("
click
")); } @Test void search_eventWithoutAlertNotDetected() { assertFalse(FindXSS.search("
click
")); } // search — URI-based detection @Test void search_detectsJavascriptUri() { assertTrue(FindXSS.search("link")); } @Test void search_detectsDataUri() { assertTrue(FindXSS.search("alert(1)\">x")); } @Test void search_normalHrefNotDetected() { assertFalse(FindXSS.search("link")); } // search — deeply nested input (#960: jtidy r938 overflows the stack, CVE-2023-34623) /** Tomcat request threads run on the JVM's default thread stack size, 1 MB on 64-bit JVMs. */ private static final long REQUEST_THREAD_STACK = 1024 * 1024; /** * Runs {@link FindXSS#search(String)} on a thread with the given stack size, so the result does * not depend on how large the test runner's own stack is, and rethrows anything it throws. */ private static boolean searchWithStack(String input, long stackSize) throws Exception { AtomicReference thrown = new AtomicReference<>(); AtomicBoolean result = new AtomicBoolean(); Thread thread = new Thread( null, () -> { try { result.set(FindXSS.search(input)); } catch (Throwable t) { thrown.set(t); } }, "findxss-test", stackSize); thread.start(); thread.join(); if (thrown.get() != null) { throw new AssertionError("search threw " + thrown.get(), thrown.get()); } return result.get(); } @Test void search_deeplyNestedBlocksDoNotOverflowTheStack() throws Exception { assertFalse( searchWithStack( "
".repeat(10_000) + "", REQUEST_THREAD_STACK)); assertFalse( searchWithStack( "
".repeat(50_000) + "", REQUEST_THREAD_STACK)); } @Test void search_reopenedInlineElementsDoNotOverflowTheStack() throws Exception { // jtidy re-opens every on its inline stack inside each new , so the tree it builds // is about n²/4 deep for n tags: on dev, about 160 '<' characters overflow a 1 MB stack. assertFalse( searchWithStack( "".repeat(100) + "", REQUEST_THREAD_STACK)); } @Test void search_inputAtTheTagLimitIsStillChecked() { // 30 + 2 '<' characters String payload = "".repeat(FindXSS.MAX_TAG_OPENERS - 2) + ""; assertTrue(FindXSS.search(payload)); } @Test void search_inputOverTheTagLimitIsNotDetected() { String payload = "".repeat(FindXSS.MAX_TAG_OPENERS - 1) + ""; assertFalse(FindXSS.search(payload)); } private static int countTags(String input) { return (int) input.chars().filter(c -> c == '<').count(); } /** Repeats the tags of {@code unit} after {@code prefix} up to MAX_TAG_OPENERS '<'s in all. */ private static String fillToTagLimit(String prefix, String unit) { String[] tags = unit.split("(?=<)"); StringBuilder input = new StringBuilder(prefix); for (int i = 0, count = countTags(prefix); count < FindXSS.MAX_TAG_OPENERS; i++, count++) { input.append(tags[i % tags.length]); } return input.toString(); } @Test void search_deepestShapesAtTheTagLimitFitInA256KbStack() throws Exception { // The shapes that build the deepest jtidy trees per '<' found while fixing #960: inline // elements re-opened in every , and s pushed again and again. If this fails, // MAX_TAG_OPENERS has been raised too far. String inlines = ""; String[] shapes = { fillToTagLimit("", ""), fillToTagLimit(inlines, ""), fillToTagLimit(inlines, ""), fillToTagLimit(inlines + "".repeat(8), ""), fillToTagLimit("".repeat(16), ""), fillToTagLimit("", "
  • "), fillToTagLimit("", "
      ") }; for (String shape : shapes) { assertEquals(FindXSS.MAX_TAG_OPENERS, countTags(shape), shape); assertFalse(searchWithStack(shape, 256 * 1024), shape); } } // findCsrfAttackUrl — path-only validation @Test void findCsrfAttackUrl_validPath() { assertTrue(FindXSS.findCsrfAttackUrl("http://localhost/admin/config", "/admin/config")); } @Test void findCsrfAttackUrl_invalidPath() { assertFalse(FindXSS.findCsrfAttackUrl("http://localhost/other/path", "/admin/config")); } @Test void findCsrfAttackUrl_malformedUrl() { assertFalse(FindXSS.findCsrfAttackUrl("not-a-url", "/admin/config")); } // findCsrfAttackUrl — path + query validation @Test void findCsrfAttackUrl_validPathAndQuery() { assertTrue( FindXSS.findCsrfAttackUrl( "http://localhost/admin/config?userId=abc123", "/admin/config", "userId", "abc123")); } @Test void findCsrfAttackUrl_wrongQueryValue() { assertFalse( FindXSS.findCsrfAttackUrl( "http://localhost/admin/config?userId=wrong", "/admin/config", "userId", "abc123")); } @Test void findCsrfAttackUrl_missingQuery() { assertFalse( FindXSS.findCsrfAttackUrl( "http://localhost/admin/config", "/admin/config", "userId", "abc123")); } // findCsrf — img tag CSRF detection @Test void findCsrf_validImgCsrf() { String msg = ""; assertTrue(FindXSS.findCsrf(msg, "fakeId123")); } @Test void findCsrf_noImgTag() { assertFalse(FindXSS.findCsrf("

      hello

      ", "fakeId")); } @Test void findCsrf_imgWithWrongPath() { String msg = ""; assertFalse(FindXSS.findCsrf(msg, "fakeId")); } @Test void findCsrf_imgWithSpacesInTags() { String msg = "< img src=\"http://localhost/root/grantComplete/csrflesson?userId=fakeId123\" / >"; assertTrue(FindXSS.findCsrf(msg, "fakeId123")); } }