From 4e26ad740e6073c164bbdb3b617b7ce5e867cea4 Mon Sep 17 00:00:00 2001 From: retoor Date: Sat, 23 May 2026 09:10:31 +0200 Subject: [PATCH] Update. --- AGENTS.md | 22 +++++++++------------- devplacepy/seo.py | 20 +++++++++++++++++--- devplacepy/static/js/ContentRenderer.js | 5 +++++ devplacepy/templates/base.html | 1 + 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 469958c..f91041a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -55,10 +55,13 @@ make locust-headless # Locust in headless CLI mode (for CI) 1. **Emoji shortcodes** → Unicode emoji (`:fire:` → 🔥, 80+ shortcodes) 2. **Markdown parse** → via `marked` with GFM tables, line breaks -3. **Code syntax highlight** → `highlight.js` on all `
` blocks
-4. **Image URLs** → standalone `.jpg/.png/.gif` URLs become `` tags
-5. **YouTube URLs** → `youtube.com/watch?v=` or `youtu.be/` become embedded iframe players
-6. **All URLs** → become `` links with `target="_blank"` and `rel="noopener"`
+3. **Sanitize** → `DOMPurify.sanitize` strips script/event-handler/iframe/`javascript:` payloads from the marked output
+4. **Code syntax highlight** → `highlight.js` on all `
` blocks
+5. **Image URLs** → standalone `.jpg/.png/.gif` URLs become `` tags
+6. **YouTube URLs** → `youtube.com/watch?v=` or `youtu.be/` become embedded iframe players
+7. **All URLs** → become `` links with `target="_blank"` and `rel="noopener"`
+
+**Sanitization is the XSS control.** Content is rendered client-side from `element.textContent`, so Jinja autoescaping does not protect it. `DOMPurify.sanitize` (vendored at `static/vendor/purify.min.js`, loaded `defer` in `base.html`) runs on the raw `marked` output before `processMedia` injects the trusted YouTube iframes, so user payloads are removed while our embeds survive. It is fail-closed: `render()` throws if `DOMPurify` is missing rather than emitting unsanitized HTML — never relax this into a `typeof` skip.
 
 **Code blocks are protected** - `NodeIterator` skips `CODE`, `PRE`, `SCRIPT`, `STYLE` elements during URL/media processing, so source code in markdown code blocks is never touched.
 
@@ -71,6 +74,7 @@ Loaded via `
 
+
 
 
 
@@ -563,19 +567,11 @@ All tests must pass. Tests stop at first failure (`-x`).
 ### Step 7: Run full suite again (only if asked by user)
 
 ```bash
-hawk .
 make test
 make test-headed   # visual confirmation
 ```
 
-### Step 8: Visual verification (if UI changed)
-
-```bash
-falcon take --output /tmp/verify.png
-falcon describe /tmp/verify.png
-```
-
-### Step 9: Document
+### Step 8: Document
 
 - Update `AGENTS.md` if new conventions introduced
 - Update `README.md` if new routes, config, or dependencies added
diff --git a/devplacepy/seo.py b/devplacepy/seo.py
index 81dee88..abd8876 100644
--- a/devplacepy/seo.py
+++ b/devplacepy/seo.py
@@ -151,6 +151,17 @@ def software_source_code_schema(gist, base_url):
     }
 
 
+def _json_ld_dumps(payload):
+    raw = json.dumps(payload, ensure_ascii=False)
+    return (
+        raw.replace("<", "\\u003c")
+           .replace(">", "\\u003e")
+           .replace("&", "\\u0026")
+           .replace("
", "\\u2028")
+           .replace("
", "\\u2029")
+    )
+
+
 def combine(schemas):
     if not schemas:
         return None
@@ -161,9 +172,12 @@ def combine(schemas):
             cleaned.append(s)
     if not cleaned:
         return None
-    if len(cleaned) == 1:
-        return json.dumps({"@context": "https://schema.org", **cleaned[0]}, ensure_ascii=False)
-    return json.dumps({"@context": "https://schema.org", "@graph": cleaned}, ensure_ascii=False)
+    payload = (
+        {"@context": "https://schema.org", **cleaned[0]}
+        if len(cleaned) == 1
+        else {"@context": "https://schema.org", "@graph": cleaned}
+    )
+    return _json_ld_dumps(payload)
 
 
 DEFAULT_OG_IMAGE = "/static/og-default.png"
diff --git a/devplacepy/static/js/ContentRenderer.js b/devplacepy/static/js/ContentRenderer.js
index 39503df..8f8c56a 100644
--- a/devplacepy/static/js/ContentRenderer.js
+++ b/devplacepy/static/js/ContentRenderer.js
@@ -51,6 +51,11 @@ export class ContentRenderer {
             html = "

" + text.replace(/\n/g, "
") + "

"; } + if (typeof DOMPurify === "undefined") { + throw new Error("DOMPurify not loaded; refusing to render untrusted HTML"); + } + html = DOMPurify.sanitize(html); + html = this.processMedia(html); return html; diff --git a/devplacepy/templates/base.html b/devplacepy/templates/base.html index 2a1ad2c..b4c476e 100644 --- a/devplacepy/templates/base.html +++ b/devplacepy/templates/base.html @@ -167,6 +167,7 @@ + {% block extra_js %}{% endblock %}