Update.
This commit is contained in:
parent
0cc22eb889
commit
4e26ad740e
22
AGENTS.md
22
AGENTS.md
@ -55,10 +55,13 @@ make locust-headless # Locust in headless CLI mode (for CI)
|
|||||||
|
|
||||||
1. **Emoji shortcodes** β Unicode emoji (`:fire:` β π₯, 80+ shortcodes)
|
1. **Emoji shortcodes** β Unicode emoji (`:fire:` β π₯, 80+ shortcodes)
|
||||||
2. **Markdown parse** β via `marked` with GFM tables, line breaks
|
2. **Markdown parse** β via `marked` with GFM tables, line breaks
|
||||||
3. **Code syntax highlight** β `highlight.js` on all `<pre><code>` blocks
|
3. **Sanitize** β `DOMPurify.sanitize` strips script/event-handler/iframe/`javascript:` payloads from the marked output
|
||||||
4. **Image URLs** β standalone `.jpg/.png/.gif` URLs become `<img>` tags
|
4. **Code syntax highlight** β `highlight.js` on all `<pre><code>` blocks
|
||||||
5. **YouTube URLs** β `youtube.com/watch?v=` or `youtu.be/` become embedded iframe players
|
5. **Image URLs** β standalone `.jpg/.png/.gif` URLs become `<img>` tags
|
||||||
6. **All URLs** β become `<a>` links with `target="_blank"` and `rel="noopener"`
|
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.
|
**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
|
```html
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/marked/lib/marked.umd.js"></script>
|
<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="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 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/ContentRenderer.js"></script>
|
||||||
<script defer src="/static/js/EmojiPicker.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)
|
### Step 7: Run full suite again (only if asked by user)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
hawk .
|
|
||||||
make test
|
make test
|
||||||
make test-headed # visual confirmation
|
make test-headed # visual confirmation
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 8: Visual verification (if UI changed)
|
### Step 8: Document
|
||||||
|
|
||||||
```bash
|
|
||||||
falcon take --output /tmp/verify.png
|
|
||||||
falcon describe /tmp/verify.png
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 9: Document
|
|
||||||
|
|
||||||
- Update `AGENTS.md` if new conventions introduced
|
- Update `AGENTS.md` if new conventions introduced
|
||||||
- Update `README.md` if new routes, config, or dependencies added
|
- Update `README.md` if new routes, config, or dependencies added
|
||||||
|
|||||||
@ -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):
|
def combine(schemas):
|
||||||
if not schemas:
|
if not schemas:
|
||||||
return None
|
return None
|
||||||
@ -161,9 +172,12 @@ def combine(schemas):
|
|||||||
cleaned.append(s)
|
cleaned.append(s)
|
||||||
if not cleaned:
|
if not cleaned:
|
||||||
return None
|
return None
|
||||||
if len(cleaned) == 1:
|
payload = (
|
||||||
return json.dumps({"@context": "https://schema.org", **cleaned[0]}, ensure_ascii=False)
|
{"@context": "https://schema.org", **cleaned[0]}
|
||||||
return json.dumps({"@context": "https://schema.org", "@graph": cleaned}, ensure_ascii=False)
|
if len(cleaned) == 1
|
||||||
|
else {"@context": "https://schema.org", "@graph": cleaned}
|
||||||
|
)
|
||||||
|
return _json_ld_dumps(payload)
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_OG_IMAGE = "/static/og-default.png"
|
DEFAULT_OG_IMAGE = "/static/og-default.png"
|
||||||
|
|||||||
@ -51,6 +51,11 @@ export class ContentRenderer {
|
|||||||
html = "<p>" + text.replace(/\n/g, "<br>") + "</p>";
|
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);
|
html = this.processMedia(html);
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
|
|||||||
@ -167,6 +167,7 @@
|
|||||||
|
|
||||||
<script defer src="/static/vendor/marked.umd.js"></script>
|
<script defer src="/static/vendor/marked.umd.js"></script>
|
||||||
<script defer src="/static/vendor/highlight.min.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/vendor/emoji-picker-element/index.js"></script>
|
||||||
<script type="module" src="/static/js/Application.js"></script>
|
<script type="module" src="/static/js/Application.js"></script>
|
||||||
{% block extra_js %}{% endblock %}
|
{% block extra_js %}{% endblock %}
|
||||||
|
|||||||
Loadingβ¦
Reference in New Issue
Block a user