Skip to content
Merged
3 changes: 1 addition & 2 deletions docs/input-validation-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ Azure/Sentinel integrations, subprocesses, or external AI providers.
| Sentinel ingestion CLI | JSON file, scan ID, finding records and environment configuration | Existing regular `.json` file under 10 MiB; at most 1,000 object findings; bounded fields; severity/config format checks |
| Azure resource data | Management-plane SDK objects | Typed SDK accessors; failures preserved as unknown; no subprocess interpolation |
| Playbook selection | Rule ID derived from stored finding | Allowlisted identifier converted to a filename and constrained beneath `playbooks/cli` |
| Website media URLs | User-entered video URL | HTTPS host allowlist and embed conversion tests in `website/test_toEmbedUrl.mjs` |
| Website editor text | Titles, excerpts, names and Markdown content | Intentionally free-form client-side content; repository write still requires the operator's GitHub token and GitHub authorization |
| Website content | Titles, excerpts, names and Markdown content authored in Decap CMS | No browser-side renderer remains: content is committed to the repository and rendered by Astro at build time, so nothing user-entered reaches a live DOM sink. Repository writes still require GitHub authorization, and the deployed pages are served under `script-src 'self'` with no inline script |

## Intentionally unrestricted text

Expand Down
9 changes: 7 additions & 2 deletions website/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,13 @@ to GitHub Pages, so the official OWASP site reflects release-controlled source.
Manual runs can deploy only when dispatched from `main`.

GitHub Pages does not support custom response headers. The document-level
content security policy covers supported directives, but hosting-level headers
such as `frame-ancestors` require a configurable hosting edge.
content security policy in `src/layouts/Base.astro` covers only the directives
a `<meta http-equiv>` policy actually enforces. `frame-ancestors` (and
`report-uri`/`report-to`, `sandbox`) are ignored in a meta policy, so
clickjacking protection is **not** in place: it needs a real
`Content-Security-Policy` or `X-Frame-Options` HTTP response header from a
configurable hosting edge. `scripts/verify-site.mjs` deliberately does not
assert `frame-ancestors` so CI never reports protection that does not exist.

## One-time maintainer setup

Expand Down
16 changes: 16 additions & 0 deletions website/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,23 @@ export default defineConfig({
site: 'https://owasp.github.io',
base: '/openshield',
integrations: [sitemap()],
build: {
// Never inline bundled CSS into the HTML. The site's CSP
// (src/layouts/Base.astro) is script-src 'self' with no 'unsafe-inline'
// and no nonce/hash - an inlined <script> block would be silently blocked
// by the browser on the deployed GitHub Pages site. Emitting every script
// as a same-origin file keeps it inside 'self'. verify-site.mjs also fails
// the build if an executable inline <script> slips into any page.
inlineStylesheets: 'never',
},
vite: {
build: {
// The setting that actually stops inlining: keep Vite from emitting
// small chunks as data: URIs or inline script text during Astro's
// client bundle step. Astro's own build schema has no
// assetsInlineLimit key, so this is the only place it takes effect.
assetsInlineLimit: 0,
},
server: {
fs: {
// repoData.ts reads scanner/rules, playbooks and docs from the repo root
Expand Down
53 changes: 53 additions & 0 deletions website/scripts/verify-site.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,59 @@ for (const file of htmlFiles) {
if (!html.includes('href="#main-content"')) failures.push(`${relative} has no skip link`);
if (!html.includes('<meta name="description"')) failures.push(`${relative} has no meta description`);
if (!html.includes('<link rel="canonical"')) failures.push(`${relative} has no canonical URL`);
const csp = html.match(/<meta http-equiv="Content-Security-Policy" content="([^"]+)"/i)?.[1] || '';
if (!csp) failures.push(`${relative} has no Content-Security-Policy meta policy`);
const scriptPolicy = csp.split(';').find((directive) => directive.trim().toLowerCase().startsWith('script-src')) || '';
// This site deliberately permits scripts from its own origin only. Checking
// merely for the absence of unsafe-inline would allow a future remote source
// to weaken the CSP without failing the production-build verification.
if (scriptPolicy.trim() !== "script-src 'self'") {
failures.push(`${relative} must use exactly script-src 'self' in its Content-Security-Policy`);
}
// script-src 'self' with no 'unsafe-inline'/nonce/hash means the browser
// silently blocks any inline script on the deployed site. Checking the CSP
// string alone would pass while the page is actually broken, so assert the
// built HTML carries no executable inline script - every script must be an
// external same-origin file (data blocks like application/json and
// application/ld+json are not executed and are fine). Astro is configured
// (vite.build.assetsInlineLimit: 0) to emit hoisted scripts as files for
// exactly this reason. Scanned by hand rather than a tag regex so this is not a
// brittle HTML filter (CodeQL js/bad-tag-filter): a case-insensitive index
// walk from each opening tag to its closing tag.
const lower = html.toLowerCase();
for (let open = lower.indexOf('<script'); open !== -1; open = lower.indexOf('<script', open + 7)) {
const tagEnd = html.indexOf('>', open);
if (tagEnd === -1) break;
const attrs = html.slice(open + 7, tagEnd);
const close = lower.indexOf('</script', tagEnd);
const body = close === -1 ? '' : html.slice(tagEnd + 1, close).trim();
const srcMatch = /\bsrc\s*=\s*["']?([^"'\s>]+)/i.exec(attrs);
if (srcMatch) {
// An external script is only loadable under script-src 'self'; a remote
// src would build green and then be CSP-blocked in the browser, which is
// the same broken-but-verified class this scan exists to prevent.
if (!srcMatch[1].startsWith('/openshield/')) {
failures.push(`${relative} loads script ${srcMatch[1]}, which script-src 'self' will block on the deployed site`);
}
continue;
}
const typeMatch = /\btype\s*=\s*["']?([^"'\s>]+)/i.exec(attrs);
const type = typeMatch ? typeMatch[1].toLowerCase() : '';
if (type === 'application/json' || type === 'application/ld+json' || type === 'speculationrules') continue;
if (body) {
failures.push(`${relative} has an inline script element that script-src 'self' will block on the deployed site`);
break;
}
}
// Only directives a <meta http-equiv> CSP actually enforces. frame-ancestors
// is deliberately absent: browsers ignore it in a meta policy, and GitHub
// Pages cannot set the HTTP response header that would make it effective, so
// asserting its presence here would report clickjacking protection that does
// not exist. That protection has to come from a real edge/header if the site
// ever moves to configurable hosting.
for (const directive of ['object-src \'none\'', 'base-uri \'self\'', 'form-action \'self\'']) {
if (!csp.includes(directive)) failures.push(`${relative} is missing CSP directive ${directive}`);
}
for (const match of html.matchAll(/href="([^"]+)"/g)) {
const href = match[1];
if (!href.startsWith('/openshield/')) continue;
Expand Down
13 changes: 9 additions & 4 deletions website/src/layouts/Base.astro
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,16 @@ const siteJsonLd = {
{breadcrumbs && <script type="application/ld+json" set:html={JSON.stringify(breadcrumbs)} is:inline></script>}

<!--
GitHub Pages cannot configure response headers. This meta policy covers the
directives browsers support in document metadata. Hosting-level directives
must be added if the deployment moves to a configurable edge.
GitHub Pages cannot configure response headers, so this policy ships in a
<meta http-equiv> tag. Per the CSP spec, a meta policy enforces only the
directives below; frame-ancestors, report-uri/report-to, and sandbox are
ignored when delivered this way. Clickjacking protection therefore requires
a real Content-Security-Policy (or X-Frame-Options) HTTP response header and
must be added at the edge if this site ever moves to configurable hosting;
a frame-ancestors directive here would only give a false sense of coverage
(verify-site.mjs intentionally does not check for it).
-->
<meta http-equiv="Content-Security-Policy" content={`default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; font-src 'self' data:; img-src 'self' data:; connect-src 'self' https://api.github.com; object-src 'none'; base-uri 'self'`}>
<meta http-equiv="Content-Security-Policy" content={`default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self' data:; img-src 'self' data:; connect-src 'self' https://api.github.com; object-src 'none'; base-uri 'self'; form-action 'self'`}>
<meta name="referrer" content="strict-origin-when-cross-origin">
</head>
<body>
Expand Down
Loading