Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .cspell.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ words:
- retag
- srcdoc
- vbscript
# Inkscape sodipodi:namedview attributes on the SVG metadata allowlist.
- bordercolor
- borderopacity
- gridtolerance
- guidetolerance
- objecttolerance
- pagecolor
- showgrid
- rescans
- endusers
- docusaurus
- kubestellar
Expand Down
170 changes: 127 additions & 43 deletions CONTENT-SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,31 +85,98 @@ Two consequences matter when a submission is rejected:
omitted: it is an effectively irreversible commitment over the apex domain and
belongs to whoever administers `cncf.io`.
- A parser can still disagree with a browser on exotic input. The gates fail
closed on anything the parser rejects, and every historical bypass is kept as
a regression fixture in `tests/svg-active-content.test.mjs` and
`tests/mdx-active-content.test.mjs`. A new bypass belongs in those fixtures
and, if it needs a new rule, in the parser walk, not in a new regular
expression.
closed on anything the parser rejects, and both gates are allowlists: an
element, attribute, node type or URL scheme is rejected unless it is listed
below, so a construct nobody has thought of yet fails rather than passes.
`tests/active-content-allowlist.test.mjs` generates arbitrary names and
schemes and checks that nothing outside the lists survives either gate, and
keeps the historical bypasses as named rows. Widening a list is a reviewed
change to the module that owns it and to this page, which a contract test
holds to the module.
- Inside an allowed `style` attribute or `<style>` block only URLs are checked;
CSS properties themselves are not allowlisted.

## SVG rules (`active content: ...` findings)

`validate:architecture-assets` rejects a diagram that contains any of the
following. The scanner tolerates namespace prefixes, entity-encoded scheme
characters, and whitespace obfuscation, so re-spelling a construct does not help
— remove it.

- **Script-bearing or embedding elements**: `<script>`, `<handler>`,
`<listener>`, `<iframe>`, `<embed>`, `<object>`, `<base>` (re-points every
relative reference in the document at a host the submitter chose), `<meta>`
(carries an http-equiv refresh).
- **Event-handler attributes**: any `on*` attribute (`onload`, `onclick`, ...).
- **Script-executing URI schemes** in any attribute: `javascript:`, `vbscript:`,
`livescript:`, `mocha:`, and `data:` URIs carrying a markup or script media
type (`text/html`, `text/xml`, `application/xml`, `text/xsl`, any `+xml` type
such as `image/svg+xml`, and JavaScript types).
- **Embedded-document attributes**: `srcdoc`.
- **`<animate>`/`<set>` targeting `href`**: an animation can install a script
URI at runtime on an element that looks inert in the source.
`validate:architecture-assets` accepts a diagram only if everything in it is on
the allowlist below; anything else is a finding, whether or not it is known to
be dangerous. The lists are the SVG and HTML vocabulary the committed diagrams
and logos actually use, plus the closely related names editors emit for the same
drawings. Names are matched exactly, by namespace and case, so re-spelling a
construct or moving it under another prefix does not help -- remove it, or ask
for it to be reviewed and added.

- **SVG elements**: `a`, `circle`, `clipPath`, `defs`, `desc`, `ellipse`,
`feBlend`, `feColorMatrix`, `feComposite`, `feDropShadow`, `feFlood`,
`feGaussianBlur`, `feMerge`, `feMergeNode`, `feOffset`, `filter`,
`foreignObject`, `g`, `image`, `line`, `linearGradient`, `marker`, `mask`,
`metadata`, `path`, `pattern`, `polygon`, `polyline`, `radialGradient`,
`rect`, `stop`, `style`, `svg`, `switch`, `symbol`, `text`, `textPath`,
`title`, `tspan`, `use`.
- **SVG attributes**: `alignment-baseline`, `aria-describedby`, `aria-hidden`,
`aria-label`, `aria-labelledby`, `baseline-shift`, `baseProfile`, `class`,
`clip`, `clip-path`, `clip-rule`, `clipPathUnits`, `color`,
`color-interpolation`, `color-interpolation-filters`, `color-rendering`, `cx`,
`cy`, `d`, `direction`, `display`, `dominant-baseline`, `dx`, `dy`,
`enable-background`, `fill`, `fill-opacity`, `fill-rule`, `filter`,
`filterUnits`, `flood-color`, `flood-opacity`, `font-family`, `font-size`,
`font-size-adjust`, `font-stretch`, `font-style`, `font-variant`,
`font-weight`, `fr`, `fx`, `fy`, `gradientTransform`, `gradientUnits`,
`height`, `href`, `id`, `image-rendering`, `in`, `in2`, `isolation`, `k1`,
`k2`, `k3`, `k4`, `lang`, `lengthAdjust`, `letter-spacing`, `lighting-color`,
`marker-end`, `marker-mid`, `marker-start`, `markerHeight`, `markerUnits`,
`markerWidth`, `mask`, `maskContentUnits`, `maskUnits`, `mix-blend-mode`,
`mode`, `offset`, `opacity`, `operator`, `orient`, `overflow`, `paint-order`,
`pathLength`, `patternContentUnits`, `patternTransform`, `patternUnits`,
`pointer-events`, `points`, `preserveAspectRatio`, `primitiveUnits`, `r`,
`refX`, `refY`, `requiredExtensions`, `requiredFeatures`, `result`, `role`,
`rotate`, `rx`, `ry`, `shape-rendering`, `spreadMethod`, `startOffset`,
`stdDeviation`, `stop-color`, `stop-opacity`, `stroke`, `stroke-dasharray`,
`stroke-dashoffset`, `stroke-linecap`, `stroke-linejoin`, `stroke-miterlimit`,
`stroke-opacity`, `stroke-width`, `style`, `systemLanguage`, `target`,
`text-anchor`, `text-decoration`, `text-rendering`, `textLength`, `transform`,
`transform-origin`, `type`, `unicode-bidi`, `values`, `vector-effect`,
`version`, `viewBox`, `visibility`, `width`, `word-spacing`, `writing-mode`,
`x`, `x1`, `x2`, `y`, `y1`, `y2`, and any `data-*` attribute.
- **Root-only SVG attributes**: `content` (the draw.io diagram source, which
`--fix` strips).
- **HTML elements inside `<foreignObject>`**: `a`, `b`, `br`, `code`, `div`,
`em`, `font`, `hr`, `i`, `li`, `ol`, `p`, `pre`, `s`, `span`, `strike`,
`strong`, `sub`, `sup`, `u`, `ul`, in the XHTML namespace.
- **HTML attributes**: `align`, `class`, `color`, `dir`, `face`, `href`, `id`,
`lang`, `size`, `style`, `target`, `title`, and any `data-*` attribute.
- **Prefixed attributes**: `xlink:href`, `xlink:title`, `xml:lang`, `xml:space`.
- **Editor metadata namespaces**: `http://creativecommons.org/ns#`,
`http://ns.adobe.com/AdobeIllustrator/10.0/`,
`http://ns.adobe.com/Extensibility/1.0/`, `http://ns.adobe.com/Graphs/1.0/`,
`http://ns.adobe.com/SaveForWeb/1.0/`, `http://purl.org/dc/elements/1.1/`,
`http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd`,
`http://www.inkscape.org/namespaces/inkscape`,
`http://www.w3.org/1999/02/22-rdf-syntax-ns#`. Any element or prefixed
attribute in one of these is allowed (browsers attach no behaviour to them),
and its descendants are still checked.
- **Unprefixed attributes on editor metadata elements**: `bleed`, `bordercolor`,
`borderopacity`, `bottomLeftOrigin`, `gridtolerance`, `guidetolerance`,
`height`, `id`, `margin`, `objecttolerance`, `pagecolor`, `showgrid`, `units`,
`width`, `x`, `y`.
- **URI schemes**: every `href` (plain or `xlink:`) and every CSS `url()`,
`image-set()` or `@import` target, in any attribute or `<style>` block, must
be relative, a `#fragment`, or use `http:`, `https:`, `mailto:`. `data:` is
allowed only for raster images: `data:image/avif`, `data:image/bmp`,
`data:image/gif`, `data:image/jpeg`, `data:image/png`, `data:image/webp`. A
malformed `url()` token is rejected too, because the scanner cannot tell what
it names.

A namespace may only be declared (`xmlns:...`) if it is one of the above, the
SVG namespace, XHTML, XLink or XML; an undeclared prefix makes the file not
well-formed. Notably absent, because each one runs script, loads content or
rewrites the document: `<script>`, `<animate>`, `<set>`, `<feImage>`,
`<iframe>`, `<object>`, `<embed>`, `<base>`, `<meta>`, form controls, every
`on*` handler, `src`, `srcset`, `srcdoc`, `action`, `formaction`, `cursor` and
`xml:base`.

The scanner also rejects, independently of the allowlist:

- **Processing instructions other than the XML declaration**: for example
`<?xml-stylesheet ...?>`, which can fetch a remote stylesheet or apply an XSLT
program to the image.
Expand All @@ -120,17 +187,24 @@ characters, and whitespace obfuscation, so re-spelling a construct does not help
faithfully (UTF-16, declared exotic encodings, NUL or replacement characters)
is rejected rather than certified blind.

Findings read `contains a disallowed element <name>`,
`contains a disallowed attribute name on <element>` or
`contains a disallowed URI scheme in name on <element>: scheme:`. When the
importer mirrors an SVG it strips everything off the allowlist, rescans the
result, and fails the import if anything is left.

**`<foreignObject>` itself is allowed.** Editors such as draw.io emit it for
ordinary text labels, and imported diagrams rely on it. Only the embedding
elements listed above are banned inside it.
ordinary text labels, and imported diagrams rely on it. The HTML inside it must
declare the XHTML namespace and stay within the HTML lists above.

## SVG remote references (`remote reference: ...` findings)

A diagram must not make a visitor's browser fetch anything from another host.
The scanner flags, in any attribute or `<style>` block:

- `href`/`src` attributes resolving to another host, including protocol-relative
(`//host/...`) and backslash-obfuscated (`https:\\host\...`) spellings;
- `href` attributes (on anything but a `<a>` hyperlink) resolving to another
host, including protocol-relative (`//host/...`) and backslash-obfuscated
(`https:\\host\...`) spellings;
- CSS `url(...)`, bare-string `@import`, and `image-set()` targets, including
forms hidden behind CSS escapes or comments — presentation attributes such as
`fill="url(https://...)"` count the same as `style`.
Expand All @@ -148,31 +222,41 @@ Local references (`#gradient`, `images/foo.png`, `/img/...`) are fine.

## Markdown/MDX page rules (`active content in imported page`)

`validate:architectures` scans imported page bodies. Allowed:

- Plain CommonMark/GFM prose, tables, images, and links.
- A small set of inert inline HTML elements: `b`, `br`, `code`, `em`, `hr`, `i`,
`kbd`, `p`, `small`, `strong`, `sub`, `sup`, `u`.
- The `CNCFProjectCard` component and its import line, which the importer itself
emits.

Rejected: any other raw HTML/JSX element, and any braced `{expression}` (MDX
evaluates it as JavaScript). Literal `{` in prose must be escaped or reworded;
the check fails closed.
`validate:architectures` scans imported page bodies against three allowlists.
Anything not on them is rejected, including node kinds, elements and schemes
nobody has reviewed yet.

- **Markdown node types**: `blockquote`, `break`, `code`, `definition`,
`delete`, `emphasis`, `footnoteDefinition`, `footnoteReference`, `heading`,
`image`, `imageReference`, `inlineCode`, `link`, `linkReference`, `list`,
`listItem`, `paragraph`, `root`, `strong`, `table`, `tableCell`, `tableRow`,
`text`, `thematicBreak`: plain CommonMark/GFM prose, tables, images, links and
code.
- **Inert inline HTML elements**: `b`, `br`, `code`, `em`, `hr`, `i`, `kbd`,
`p`, `small`, `strong`, `sub`, `sup`, `u`, with no attributes.
- **Component**: the `CNCFProjectCard` component and its import line, which the
importer itself emits, with the attributes `description`, `href`, `logo`,
`name`, `since`, `version`, each a plain string.
- **URL schemes**: every link, image and definition destination, and the card
`href` and `logo`, must be relative, a `#fragment`, or use `http:`, `https:`,
`mailto:`.

Rejected: any other raw HTML/JSX element or attribute, any other import or
export, any braced `{expression}` (MDX evaluates it as JavaScript), and any
` ```mdx-code-block ` fence (Docusaurus compiles its body as MDX). Literal `{`
in prose must be escaped or reworded; the check fails closed. Findings read
`disallowed element <name>`, `disallowed attribute name on <element>`,
`disallowed node type name` or `disallowed URL scheme scheme:`.

The inline-element list above is matched exactly as written, in lower case. MDX
reads a lower-case tag name as an HTML element and any other casing as a
_component_ reference, so `<B>` is not the inert `<b>` the list allows — it
compiles to a lookup of a component named `B` that nothing defines, and the
build fails while rendering the page. Write these elements in lower case.

A `style` attribute is rejected on every element. React requires `style` to be
an object rather than a string, so `style="..."` (and `style={"..."}`) fails
while rendering, and the object form `style={{ ... }}` is a braced expression,
which is rejected above. Use the stylesheet-backed markup instead.

Image references in page bodies are rewritten during import: relative paths are
scoped to the architecture's own asset directory, and an image whose destination
scoped to the architecture's own asset directory, an image whose scheme is not
on the list above is reduced to its alt text, and an image whose destination
names any host — including protocol-relative `//host/...` — is demoted to a
plain link unless it is a cncf/artwork URL the importer can mirror. Images are
found in the parse tree, so reference-style images (`![alt][ref]`) follow the
Expand Down
6 changes: 6 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ actions (the `actions` language) on pushes to main, pull requests that touch
either, and a weekly schedule. Report all vulnerabilities in third-party
dependencies here, including ones with no fixed version available yet.

Imported architecture pages and diagrams pass allowlist gates before they are
published: an SVG element, attribute or namespace, an MDX node type or element,
or a URL scheme that is not explicitly listed is rejected or stripped.
[CONTENT-SECURITY.md](CONTENT-SECURITY.md) lists every allowlist and the
residual risks. A way to publish anything outside those lists is in scope.

## Response headers

The site is served by Netlify, and [`netlify.toml`](netlify.toml) sets every
Expand Down
41 changes: 41 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
"@swc/core": "1.16.18",
"@swc/core-linux-x64-gnu": "1.16.18",
"cspell": "^10.4.0",
"fast-check": "^4.10.2",
"jscpd": "^5.4.1",
"markdown-link-check": "3.15.0",
"markdownlint": "^0.42.0",
Expand Down
23 changes: 12 additions & 11 deletions scripts/lib/architecture-content.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
findRemoteReferences,
stripActiveContent,
} from './svg-active-content.mjs';
import { activeScheme } from './uri-safety.mjs';
import { disallowedScheme } from './uri-safety.mjs';

/**
* A Markdown destination that carries its own authority: either an absolute
Expand Down Expand Up @@ -55,20 +55,21 @@ function destination(url) {
* all seen as the parser sees them. Each image is replaced by exactly the
* source range it occupies.
*
* - A script-capable destination is reduced to its alt text, and that test
* comes first. `javascript://%0aalert(1)` names no host, but it matches
* REMOTE_DESTINATION exactly as `https://host/x` does, so deciding the host
* question first demotes it to a *link* carrying the same scheme — `//` is a
* JavaScript line comment, so that link runs on click. Whether a value runs
* script is activeScheme()'s question (scripts/lib/uri-safety.mjs), the same
* source of truth the SVG and MDX gates ask, and it is asked here in the
* same order they ask it.
* - A destination whose scheme is not on the allowlist is reduced to its alt
* text, and that test comes first. `javascript://%0aalert(1)` names no host,
* but it matches REMOTE_DESTINATION exactly as `https://host/x` does, so
* deciding the host question first demotes it to a *link* carrying the same
* scheme — `//` is a JavaScript line comment, so that link runs on click.
* Which schemes are allowed is disallowedScheme()'s question
* (scripts/lib/uri-safety.mjs), the same source of truth the SVG and MDX
* gates ask, and it is asked here in the same order they ask it.
* - A destination that names a host (REMOTE_DESTINATION, which includes
* protocol-relative and any-case schemes) is mirrored when projectAsset()
* resolves it and otherwise demoted to a plain link, or to its alt text
* inside an existing link. A published `<img>` is fetched by every visitor's
* browser, so a third-party host would see their IP, User-Agent and Referer.
* - Any other scheme (`data:`, `ftp:`, ...) is reduced to its alt text.
* - An allowed scheme that names no host (`mailto:`, `https:x`) is reduced to
* its alt text.
* - A relative path is rebased onto `/img/architectures/<id>/`.
*/
function rewriteImages(body, id) {
Expand Down Expand Up @@ -102,7 +103,7 @@ function rewriteImages(body, id) {
const { alt } = node;
let replacement;

if (activeScheme(url)) {
if (disallowedScheme(url)) {
replacement = escapeLabel(alt);
} else if (REMOTE_DESTINATION.test(url)) {
const asset = projectAsset(url);
Expand Down
Loading
Loading