diff --git a/docs/dependency-upgrade-plan.md b/docs/dependency-upgrade-plan.md index 7f34eb48..9809eb88 100644 --- a/docs/dependency-upgrade-plan.md +++ b/docs/dependency-upgrade-plan.md @@ -655,9 +655,9 @@ detail needed to act on them; kept here as the record of what this phase chose n - [ ] Burn down the 263 `vue-tsc` errors. Now two distinct groups: ~208 pre-existing (149 in `composables/useDirectus.ts`) and **55 newly surfaced `noUncheckedIndexedAccess` violations**, which are the more interesting set — each is a real unchecked index. -- [ ] Stop the prerender crawler walking image URLs, so a full local build works: - `nitro: { prerender: { ignore: ['/_ipx'] } }`. Pre-existing on `main`; affects only local - full builds, since CI skips discovery and Vercel does not prerender. +- [x] Stop the prerender crawler walking image URLs, so a full local build works — done 2026-08-04 + (#240): 193s and intermittently failing became 29s. Scoped to non-static builds; see the + follow-up backlog for why that guard matters. - [ ] `tailwind.config.js` `content` globs are `'./pages/**/*.{html,js}'` and `'./components/**/*.{html,js}'` — **no `.vue`**. The site is styled only because `@nuxtjs/tailwindcss` injects its own defaults over the top. Worth fixing before Tailwind 4 @@ -1340,11 +1340,26 @@ So the swap needed real text, not tag-stripped HTML: | regex + `{{ }}` (the naive fix) | ❌ shows `für` | ❌ | ✅ inert | | `getPlainText` + `{{ }}` (shipped) | ✅ `für` | ✅ `&` | ✅ inert | -`helpers/getPlainText.ts` sanitises with `ALLOWED_TAGS: []` and `RETURN_DOM_FRAGMENT`, then reads -`textContent`. That returns genuine text with every entity decoded, which is safe for `{{ }}` and must -never be handed to `v-html`. It parses instead of pattern-matching, which is the whole point: a regex -cannot match a tag containing `<` or `>`, so ` src=x onerror=alert(1)>` survived -`/<[^<>]+>/g` as a working tag. +`getPlainText` sanitises with `ALLOWED_TAGS: []` and `RETURN_DOM_FRAGMENT`, then reads `textContent`. +That returns genuine text with every entity decoded, which is safe for `{{ }}` and must never be handed +to `v-html`. It parses instead of pattern-matching, which is the whole point: a regex cannot match a tag +containing `<` or `>`, so ` src=x onerror=alert(1)>` survived `/<[^<>]+>/g` as a working tag. + +**`helpers/sanitize.ts` is the only module that touches DOMPurify**, after a review comment on #237 +pointed out that the two ProfileCreation components had each grown their own +`computed(() => DOMPurify.sanitize(...))`, duplicating `InnerHtml.vue`. It cited this repo's own rule +in `AGENTS.md` — never duplicate logic across modules — and was right. Three exports: + +| export | for | +| --- | --- | +| `sanitizeHtml` | default policy, anything bound to `v-html` | +| `sanitizeInlineHtml` | as above but forbids `

`, for the news ticker's single scrolling line | +| `getPlainText` | real plain text, for `{{ }}` | + +`InnerHtml.vue` and `NewsTicker.vue` were routed through it too, rather than fixing only the two new +duplicates — otherwise two of four policies would still have been inline, which is the appearance of +the rule rather than the rule. `NewsTicker`'s `{ FORBID_TAGS: ['p'] }` became a named export rather +than a config parameter, so DOMPurify options do not leak back out to callers. It is deliberately **not** re-exported from `helpers/index.ts`. That barrel is imported by server routes, and pulling `isomorphic-dompurify` through it would instantiate jsdom for consumers that only @@ -1363,9 +1378,11 @@ classes. So these two were inconsistent outliers rather than a deliberate choice ### Verification -Unit tests cover the bypass payloads, so the regex cannot come back unnoticed: `test/getPlainText.test.ts`, -5 cases, including ` src=x onerror=alert(1)>`, ` onload=…>` and the `