Skip to content
Closed

0.x #41

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
10 changes: 5 additions & 5 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: Build & Test

on:
push:
branches: [main]
branches: [main, '0.x']
pull_request:
branches: [main]
branches: [main, '0.x']

# Default token to read-only; jobs widen only what they need.
permissions:
Expand All @@ -21,7 +21,7 @@ jobs:
with:
egress-policy: audit
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Setup Node
Expand All @@ -48,7 +48,7 @@ jobs:
with:
egress-policy: audit
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Setup Node
Expand All @@ -58,5 +58,5 @@ jobs:
cache: npm
- run: npm ci
- run: npm run build
- run: npx playwright install --with-deps chromium
- run: npx playwright install --with-deps chromium firefox webkit
- run: npm run test:browser
6 changes: 3 additions & 3 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: CodeQL

on:
push:
branches: [main]
branches: [main, '0.x']
pull_request:
branches: [main]
branches: [main, '0.x']
schedule:
- cron: '0 19 * * 4'

Expand All @@ -29,7 +29,7 @@ jobs:
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Initialize CodeQL
Expand Down
9 changes: 5 additions & 4 deletions .github/workflows/scorecard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ name: Scorecard supply-chain security
on:
# For the Branch-Protection check; only the default branch is supported.
branch_protection_rule:
# Keeps the Maintained check fresh and re-evaluates after CodeQL has settled.
# Keeps the Maintained check fresh and re-evaluates after CodeQL has settled. Weekly is enough:
# the score signals do not move day to day, and Friday lands just after the Thursday CodeQL run.
schedule:
- cron: '24 17 * * *'
- cron: '24 17 * * 5'
# Lets a maintainer trigger the first run (and re-runs) by hand from the Actions
# tab, instead of waiting for the daily cron - useful to publish the first report.
# tab, instead of waiting for the weekly cron - useful to publish the first report.
workflow_dispatch:
# NOTE: no 'push' trigger on purpose - running at merge time races the CodeQL
# scan of the just-merged commit and makes the SAST check read as unchecked.
Expand All @@ -38,7 +39,7 @@ jobs:
with:
egress-policy: audit
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Run analysis
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/sign-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.event.release.tag_name }}
persist-credentials: false
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/slsa-provenance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.event.release.tag_name }}
persist-credentials: false
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
node_modules/
dist/fortify.cov.*
coverage/
.nyc_output/
*.log
Expand Down
225 changes: 176 additions & 49 deletions README.md

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion config/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,9 @@ export default defineConfig({
reuseExistingServer: !process.env.CI,
timeout: 30_000,
},
projects: [{ name: 'chromium', use: { browserName: 'chromium' } }],
projects: [
{ name: 'chromium', use: { browserName: 'chromium' } },
{ name: 'firefox', use: { browserName: 'firefox' } },
{ name: 'webkit', use: { browserName: 'webkit' } },
],
});
23 changes: 23 additions & 0 deletions config/rollup.coverage.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Coverage build: the ESM module instrumented with Istanbul, written to a separate file the node
* test suite imports when DOMFORTIFY_COV is set. Mirrors DOMPurify's instrument-then-nyc approach;
* Istanbul's per-file counters accumulate correctly across the suite's fresh-module-per-test imports,
* where V8 coverage would fragment on the cache-busting query.
*/
import { createRequire } from 'node:module';
import typescript from '@rollup/plugin-typescript';
import replace from '@rollup/plugin-replace';
import istanbul from 'rollup-plugin-istanbul';

const require = createRequire(import.meta.url);
const pkg = require('../package.json');

export default {
input: 'src/index.ts',
output: { file: 'dist/fortify.cov.es.mjs', format: 'es', sourcemap: true },
plugins: [
replace({ preventAssignment: true, values: { __VERSION__: pkg.version } }),
typescript({ tsconfig: './config/tsconfig.build.json' }),
istanbul({ include: ['src/**/*.ts'] }),
],
};
13 changes: 13 additions & 0 deletions demos/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,19 @@ window.DOMFortifyConfig = {
};
```

### Scoping with INCLUDE [Link](include-demo.html)

The allow-list complement of `EXCLUDE`: activate ONLY on matching URLs and stay inactive elsewhere.
Paired with `INJECT_META` so enforcement is scoped to the same pages, this is the gradual-rollout
pattern - protect a few routes first, leave the rest untouched. Add `?admin` and reload.

```js
window.DOMFortifyConfig = {
INCLUDE: [/[?&]admin\b/], // active only here
INJECT_META: true, // and enforcement scoped to the same pages
};
```

### Meta injection (best-effort) [Link](meta-inject-demo.html)

`INJECT_META` is an opt-in attempt to add the enabling CSP `<meta>` for pages that can set neither a
Expand Down
83 changes: 83 additions & 0 deletions demos/include-demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>DOMFortify demo: Scoping with INCLUDE</title>
<link rel="stylesheet" href="lib/demo.css" />
<script src="https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js"></script>
<!--
No static enforcement <meta> here on purpose. INCLUDE scopes WHERE DOMFortify activates, and it
only composes safely when enforcement is scoped the same way - so we let DOMFortify turn
enforcement on itself, per page, with INJECT_META. On an out-of-scope URL it stays inactive AND
injects nothing, so that page is simply left untouched (unprotected, not broken). This is the
gradual-rollout pattern: protect /admin first, leave everything else alone.
-->
<script>
window.DOMFortifyConfig = {
INCLUDE: [/[?&]admin\b/], // activate ONLY where the URL matches
INJECT_META: true, // and let DOMFortify scope enforcement to those same pages
SANITIZER_CONFIG: { ALLOWED_TAGS: ['b', 'i', 'p', 'a', '#text'], ALLOWED_ATTR: ['href'] },
};
</script>
<script src="https://cdn.jsdelivr.net/gh/cure53/DOMFortify@main/dist/fortify.js"></script>
</head>
<body>
<main>
<nav class="crumb"><a href="README.md">DOMFortify demos</a> / Scoping with INCLUDE</nav>
<h1>Scoping with INCLUDE</h1>
<p class="lead">
<code>INCLUDE</code> is the allow-list complement of <code>EXCLUDE</code>: DOMFortify activates
<em>only</em> on matching URLs and stays inactive everywhere else. This page keys off the query
string. <code>?admin</code> is in scope, so DOMFortify claims the policy, injects the enabling
CSP, and sanitizes. The baseline URL is out of scope, so DOMFortify stands down and leaves the
page untouched.
</p>

<div class="card">
<h2>Pick a URL</h2>
<p>
<a href="?">baseline (out of scope)</a> &nbsp;|&nbsp;
<a href="?admin">?admin (in scope)</a>
</p>
<p class="muted">Current: <code id="href"></code></p>
<p class="muted">excluded = <span id="excluded"></span> &nbsp; metaInjected =
<span id="meta"></span> &nbsp; protected = <span id="protected"></span></p>
</div>

<div class="card">
<h2>Same payload on both URLs</h2>
<textarea id="dirty">&lt;p&gt;&lt;b&gt;bold&lt;/b&gt; &lt;a href="https://example.com"&gt;link&lt;/a&gt;&lt;/p&gt;&lt;img src=x onerror="alert(1)"&gt;</textarea>
<button id="run">Run the sink</button>
<div class="muted">Resulting HTML:</div>
<pre id="html"></pre>
<p id="note"></p>
</div>
</main>
<script>
window.alert = () => {}; // so an out-of-scope, unprotected page cannot actually pop a dialog
const s = (window.DOMFortify && DOMFortify.status()) || {};
document.getElementById('href').textContent = location.href;
document.getElementById('excluded').textContent = String(!!s.excluded);
document.getElementById('meta').textContent = String(!!s.metaInjected);
document.getElementById('protected').textContent = String(!!s.protected);

const out = document.createElement('div');
const run = () => {
const note = document.getElementById('note');
try {
out.innerHTML = document.getElementById('dirty').value;
document.getElementById('html').textContent = out.innerHTML;
note.innerHTML = s.protected
? '<span class="status ok">In INCLUDE scope: enforcement injected, sink sanitized.</span>'
: '<span class="status bad">Out of INCLUDE scope: DOMFortify stood down, so this page is unprotected (the raw markup went straight in). On a real site, /admin would be protected and this route left as-is.</span>';
} catch (e) {
document.getElementById('html').textContent = '';
note.innerHTML = '<span class="status bad">Sink threw: ' + e.message + '</span>';
}
};
document.getElementById('run').addEventListener('click', run);
run();
</script>
</body>
</html>
Loading