Skip to content
Open
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: 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
15 changes: 15 additions & 0 deletions website/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,22 @@ export default defineConfig({
site: 'https://owasp.github.io',
base: '/openshield',
integrations: [sitemap()],
build: {
// Never inline hoisted <script> or 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',
assetsInlineLimit: 0,
},
vite: {
build: {
// Same reason: stop Vite from inlining small chunks as data: URIs or
// inline script text during Astro's client bundle step.
assetsInlineLimit: 0,
},
server: {
fs: {
// repoData.ts reads scanner/rules, playbooks and docs from the repo root
Expand Down
41 changes: 41 additions & 0 deletions website/scripts/verify-site.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,47 @@ 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().startsWith('script-src')) || '';
if (!scriptPolicy || scriptPolicy.includes('unsafe-inline')) {
failures.push(`${relative} allows inline script execution 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
// (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();
if (/\bsrc\s*=/i.test(attrs)) 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