diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiContentNormalizer.java b/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiContentNormalizer.java index 205bb0fd..a8a86312 100644 --- a/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiContentNormalizer.java +++ b/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiContentNormalizer.java @@ -65,10 +65,10 @@ public class WikiContentNormalizer { * Strip nav/footer/script/style/aside and ad-like classes from HTML, then * return readable text. *
- * The output never carries markup: input that still contains tags is parsed - * and stripped; input that is already tag-free is returned with whitespace - * cleanup only. When the document cannot be parsed (too large, or jsoup - * throws) it is run through {@link #stripMarkupLossy(String)} so that + * The output never carries markup: input that still contains element + * structure is parsed and stripped; input with no element structure is + * reduced to its text content. When the document cannot be parsed (too + * large, or jsoup throws) it is run through {@link #stripMarkupLossy(String)} so that * script/style bodies and tags are dropped without a full parse — the raw * markup is never passed through verbatim. */ @@ -92,7 +92,15 @@ public class WikiContentNormalizer { boolean hasMarkup = (body != null && !body.children().isEmpty()) || (head != null && !head.children().isEmpty()); if (!hasMarkup) { - return collapseBlankLines(rawHtml); + // No element structure — text that was already tag-stripped upstream + // (an extracted .html/.htm upload). Return its whole text rather than + // the raw string: wholeText() keeps the original line breaks (so ATX + // headings stay on their own lines) but carries no tags, no attributes + // and no comment nodes. The raw string must never be returned here — + // a bare /
skeleton can still hold event-handler + // attributes that would otherwise leak through verbatim. + Element textRoot = body != null ? body : doc; + return collapseBlankLines(textRoot.wholeText()); } // Drop structural noise. diff --git a/mateclaw-server/src/test/java/vip/mate/wiki/service/WikiContentNormalizerSecurityTest.java b/mateclaw-server/src/test/java/vip/mate/wiki/service/WikiContentNormalizerSecurityTest.java new file mode 100644 index 00000000..9fe7d08a --- /dev/null +++ b/mateclaw-server/src/test/java/vip/mate/wiki/service/WikiContentNormalizerSecurityTest.java @@ -0,0 +1,215 @@ +package vip.mate.wiki.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.regex.Pattern; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Adversarial tests for {@link WikiContentNormalizer}. + *
+ * Each test feeds hostile HTML through the html / htm / url normalization path
+ * and asserts that no executable markup — tags, event-handler attributes, or
+ * script/style bodies — survives into the normalized output, while legitimate
+ * prose and headings are preserved. The normalized output is plain text by
+ * contract, so it must contain no tag tokens at all.
+ */
+class WikiContentNormalizerSecurityTest {
+
+ private WikiContentNormalizer normalizer;
+
+ /** Any HTML start or end tag token ({@code in is fully removed")
+ void scriptInHead() {
+ String p = ""
+ + " Body text. Before. After. Visible. Visible. Body.Doc
x
"; + assertNoLiveMarkup("onclick-heading", p); + String out = normalizer.normalize("html", p); + assertTrue(out.contains("## Section Title"), "heading text and level survive"); + } + + @Test + @DisplayName("nested / mutation script tags do not yield a live tag") + void mutationScript() { + assertNoLiveMarkup("mutation", + "Body.
"); + } + + @Test + @DisplayName("noscript-wrapped script is removed") + void noscriptWrappedScript() { + assertNoLiveMarkup("noscript", + "Body.
"); + } + + @Test + @DisplayName("object and embed elements do not survive") + void objectAndEmbed() { + assertNoLiveMarkup("object-embed", + "" + + "