This commit is contained in:
retoor 2026-05-23 09:10:31 +02:00
parent 0cc22eb889
commit 4e26ad740e
4 changed files with 32 additions and 16 deletions

View File

@ -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 `<pre><code>` blocks
4. **Image URLs** β†’ standalone `.jpg/.png/.gif` URLs become `<img>` tags
5. **YouTube URLs** β†’ `youtube.com/watch?v=` or `youtu.be/` become embedded iframe players
6. **All URLs** β†’ become `<a>` 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 `<pre><code>` blocks
5. **Image URLs** β†’ standalone `.jpg/.png/.gif` URLs become `<img>` tags
6. **YouTube URLs** β†’ `youtube.com/watch?v=` or `youtu.be/` become embedded iframe players
7. **All URLs** β†’ become `<a>` 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 `<script>` tags in `base.html`. ALL must use `defer` to avoid blockin
```html
<script defer src="https://cdn.jsdelivr.net/npm/marked/lib/marked.umd.js"></script>
<script defer src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/highlight.min.js"></script>
<script defer src="/static/vendor/purify.min.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/emoji-picker-element@^1/index.js"></script>
<script defer src="/static/js/ContentRenderer.js"></script>
<script defer src="/static/js/EmojiPicker.js"></script>
@ -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

View File

@ -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"

View File

@ -51,6 +51,11 @@ export class ContentRenderer {
html = "<p>" + text.replace(/\n/g, "<br>") + "</p>";
}
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;

View File

@ -167,6 +167,7 @@
<script defer src="/static/vendor/marked.umd.js"></script>
<script defer src="/static/vendor/highlight.min.js"></script>
<script defer src="/static/vendor/purify.min.js"></script>
<script type="module" src="/static/vendor/emoji-picker-element/index.js"></script>
<script type="module" src="/static/js/Application.js"></script>
{% block extra_js %}{% endblock %}