diff --git a/README.md b/README.md index b684a0f..afd5ddd 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Route paths are configuration, not files. The layer's page components live in `a - **Tag** (`journal-tag`) and **author** (`journal-author`) pages. An author resolves by author slug, or by login once the author has a published post; a login URL sets the canonical to the slug URL. - **RSS**: `/rss.xml` proxies the API feed, and list and post pages link it in the head. - **SEO**: per-page title and description, Open Graph tags (`og:image` from the featured image, article times), canonical URLs that keep `?page=N`, robots and `sitemap.xml` through `@nuxtjs/seo`. -- **HTML gate**: post bodies and category content pass the Journal API's three-pattern gate (raw script, iframe, object or embed tags; event-handler attributes; javascript, vbscript or data URLs) before `v-html`. A value that fails it renders a neutral notice and never reaches the page or the SSR payload. +- **HTML sanitizer**: post bodies and category content go through `sanitizeJournalHtml` before `v-html` and before they reach the SSR payload. It is [sanitize-html](https://github.com/apostrophecms/sanitize-html) with the Journal API's own allow-list: the usual content elements, `footer`, `class`, schema.org microdata, the `color`, `font-size`, `font-weight` and `text-align` inline styles, and `http`, `https`, `mailto` or relative URLs. Anything else (`script`, `style`, `iframe`, `svg`, forms, event-handler attributes, `javascript:` URLs in any spelling) is removed and the rest of the value is kept, so a post never renders blank because of one stray tag. Server and browser produce the same string, so hydration matches. See `docs/decisions/0001-sanitize-html.md`. - **i18n**: English and Polish UI strings, registered only for the locales the app enables. ## Usage @@ -56,7 +56,7 @@ export default defineNuxtConfig({ }) ``` -The layer declares `@nuxt/fonts`, `@nuxtjs/i18n` and `@nuxtjs/seo` itself. Do not list them again in the app; if the app must, pin the same exact versions. Static app pages such as `/about` take precedence over a `/:category` route. +The layer declares `@nuxt/fonts`, `@nuxtjs/i18n`, `@nuxtjs/seo` and `sanitize-html` itself. Do not list them again in the app; if the app must, pin the same exact versions. Static app pages such as `/about` take precedence over a `/:category` route. Run the built app with `node .output/server/index.mjs` next to the SummerCMS application and point `NUXT_JOURNAL_API_BASE` at it. @@ -111,7 +111,7 @@ Override tokens in the app's CSS. The app's `css` entries load after the layer's } ``` -Override a component by creating the same file in the app, for example `app/components/Journal/PostCard.vue`. The overridable components are `JournalPostBody`, `JournalPostCard`, `JournalPostList`, `JournalPostMeta`, `JournalFeaturedImage`, `JournalPagination`, `JournalRelatedPosts`, `JournalIndexHeader`, `JournalCategoryHeader`, `JournalAuthorCard` and `JournalRssLink`. A replacement that renders HTML from the API must keep the gate (`gateJournalHtml`) in front of `v-html`. +Override a component by creating the same file in the app, for example `app/components/Journal/PostCard.vue`. The overridable components are `JournalPostBody`, `JournalPostCard`, `JournalPostList`, `JournalPostMeta`, `JournalFeaturedImage`, `JournalPagination`, `JournalRelatedPosts`, `JournalIndexHeader`, `JournalCategoryHeader`, `JournalAuthorCard` and `JournalRssLink`. A replacement that renders HTML from the API must pass it through `sanitizeJournalHtml` (or `gateJournalHtml`, whose `html` is the same sanitized string) before `v-html`. Style classes that imported content uses, such as an author footer, belong in the app's CSS scoped under `.journal-post-body`: the sanitizer removes `
keep
'], + ['css url in style attribute', 'keep
'], + ['css expression in colour', 'keep
'], + ['noscript title breakout', 'keep
'], + ['handler in upper case', 'keep
'], +] + +for (const [name, html] of [...GO_CASES, ...REVIEW_BYPASSES]) { + test(`${name} is neutralized and the rest is kept`, () => { + const out = assertNeutralized(html, name) + assert.match(out, /keep/, `${name} lost the safe text: ${out}`) }) } -test('object and embed tags and spaced handlers are unsafe', () => { - assert.equal(isSafeJournalHtml(''), false) - assert.equal(isSafeJournalHtml('