You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* feat(security): implement Content Security Policy
- Add kit.csp to svelte.config.js with mode: 'hash':
default-src/connect-src/img-src 'self'; script-src with hashes;
style-src 'self'; font-src/object-src 'none'; base-uri/form-action
'self'; frame-ancestors 'none'; upgrade-insecure-requests
- CSP delivered as HTTP header (Cloudflare Worker SSR) so frame-ancestors
is enforced; X-Frame-Options remains as legacy fallback
- Replace style="display: contents" in app.html with Tailwind contents
class to eliminate the one inline style attribute
- Manually hash the theme-detection inline script in app.html; SvelteKit
auto-hashes its bootstrap script per render
- Document method restriction (405 on non-GET), CSP reporting gap,
and coordinated disclosure policy in SECURITY.md
- Restructure SECURITY.md: new Runtime protections section between
How the application works and Supply chain controls
* chore: add security.txt (RFC 9116)
Copy file name to clipboardExpand all lines: SECURITY.md
+38-13Lines changed: 38 additions & 13 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -23,6 +23,8 @@ Include:
23
23
24
24
You can expect an acknowledgement within 48 hours and a resolution or status update within 7 days.
25
25
26
+
**Disclosure policy:** Reported vulnerabilities are kept confidential until a fix is deployed. The default embargo window is 90 days from initial report, though critical issues affecting user data will be patched and disclosed faster. After a fix ships, a GitHub Security Advisory will be published. CVE assignment will be requested for vulnerabilities with a CVSS score of 7.0 or higher.
27
+
26
28
---
27
29
28
30
## Threat model
@@ -31,7 +33,7 @@ You can expect an acknowledgement within 48 hours and a resolution or status upd
31
33
32
34
| Adversary | Goal | Primary controls |
33
35
|---|---|---|
34
-
| Malicious paste / shared link | Trigger XSS via crafted SAML or JWT content rendered in the DOM | Svelte's default text escaping, `esc()` in the XML highlighter, CSP (planned) |
36
+
| Malicious paste / shared link | Trigger XSS via crafted SAML or JWT content rendered in the DOM | Svelte's default text escaping, `esc()` in the XML highlighter, CSP (`default-src 'self'`, no `unsafe-inline`, hash-pinned inline scripts) |
35
37
| Compromised npm package | Inject malicious code into the build or CI environment | GuardDog, OSV-Scanner, Grype, `npm audit`, SHA-pinned actions, `npm ci` lockfile enforcement |
36
38
| Compromised upstream GitHub Action | Substitute malicious CI code via a tampered version tag | All actions pinned to commit SHA, Dependabot rotates pins daily |
37
39
| SSRF via OIDC proxy | Use the discovery Worker to reach internal infrastructure |`redirect: 'error'`, 5-second timeout, 100 KB cap, response validation |
@@ -83,6 +85,41 @@ The Worker does not receive, log, or store any JWT token. It receives only the i
83
85
-**100 KB response cap** — the `Content-Length` header is checked before reading the body, and the body itself is capped at 100,000 bytes; oversized responses are rejected with 502
84
86
-**Response validation** — the body must be valid JSON, must be a non-null object, and must contain an `issuer` field (required by RFC 8414); anything else returns 502
85
87
-**Origin restriction** — responses include `Access-Control-Allow-Origin: https://samlguy.com`, preventing other browser origins from using the Worker as a free fetch proxy; this does not prevent the outbound request itself, only cross-origin response reads
88
+
-**Method restriction** — only `GET` is exported from the endpoint; SvelteKit returns `405 Method Not Allowed` with an `Allow: GET, HEAD` header for any other method automatically
89
+
90
+
---
91
+
92
+
## Runtime protections
93
+
94
+
### Content Security Policy
95
+
96
+
CSP is configured via `kit.csp` in `svelte.config.js` using `mode: 'hash'` and delivered as a `Content-Security-Policy` HTTP response header on every request (not a meta tag — the Cloudflare Worker SSR path sets headers directly, which means `frame-ancestors` is respected):
97
+
98
+
-`default-src 'self'` — baseline: only same-origin resources allowed
99
+
-`script-src 'self' <hashes>` — no `unsafe-inline`; SvelteKit automatically hashes its bootstrap script per render; the theme-detection script in `app.html` is covered by a manually computed static hash
100
+
-`style-src 'self'` — all styles compile to external files; no inline `<style>` tags remain in the rendered HTML
101
+
-`connect-src 'self'` — fetch calls (OIDC discovery proxy) are same-origin
102
+
-`img-src 'self'` — favicon and assets only
103
+
-`font-src 'none'` — no external fonts loaded
104
+
-`object-src 'none'` — blocks plugins
105
+
-`base-uri 'self'` — prevents base tag injection
106
+
-`form-action 'self'` — prevents form hijacking
107
+
-`frame-ancestors 'none'` — blocks the page from being embedded in any frame; delivered as an HTTP header so this directive is enforced (meta-tag CSP does not support `frame-ancestors`)
108
+
-`upgrade-insecure-requests` — instructs browsers to upgrade any HTTP sub-resource requests to HTTPS
109
+
110
+
No `report-uri` or `report-to` directive is currently configured; CSP violations in production are silent. A future improvement is a lightweight violation-reporting endpoint that drops the `document-uri` field (which would contain the URL fragment and therefore the user's paste) before logging.
111
+
112
+
### HTTP security headers
113
+
114
+
A `_headers` file at the project root is processed by `@sveltejs/adapter-cloudflare` and deployed to every Cloudflare Pages response:
115
+
116
+
-`Strict-Transport-Security: max-age=63072000; includeSubDomains; preload` — instructs browsers to use HTTPS exclusively for two years; `preload` makes the domain eligible for inclusion in browser HSTS preload lists
-`X-Frame-Options: DENY` — legacy framing protection for browsers that do not support CSP `frame-ancestors`; modern browsers use the CSP directive above
119
+
-`Referrer-Policy: no-referrer` — suppresses the `Referer` header on all outbound navigation; prevents token or payload data from leaking via URL referrers if a user clicks an external link
120
+
-`Permissions-Policy` — disables camera, microphone, and geolocation access
121
+
122
+
The adapter additionally appends `Cache-Control: public, immutable, max-age=31536000` on all `/_app/immutable/*` assets and `Cache-Control: no-cache` on mutable app assets.
86
123
87
124
---
88
125
@@ -141,18 +178,6 @@ The Cloudflare API token stored as a repository secret is scoped to Cloudflare P
141
178
142
179
The deploy job targets a GitHub Environment named `production`. This provides a configuration point for adding manual approval requirements, allowed-branch restrictions, or deployment protection rules in the future without changing any workflow code.
143
180
144
-
### HTTP security headers
145
-
146
-
A `_headers` file at the project root is processed by `@sveltejs/adapter-cloudflare` and deployed to every Cloudflare Pages response:
147
-
148
-
-`Strict-Transport-Security: max-age=63072000; includeSubDomains; preload` — instructs browsers to use HTTPS exclusively for two years; `preload` makes the domain eligible for inclusion in browser HSTS preload lists
-`X-Frame-Options: DENY` — blocks the site from being embedded in an iframe (clickjacking mitigation)
151
-
-`Referrer-Policy: no-referrer` — suppresses the `Referer` header on all outbound navigation; prevents token or payload data from leaking via URL referrers if a user clicks an external link
152
-
-`Permissions-Policy` — disables camera, microphone, and geolocation access
153
-
154
-
The adapter additionally appends `Cache-Control: public, immutable, max-age=31536000` on all `/_app/immutable/*` assets and `Cache-Control: no-cache` on mutable app assets.
155
-
156
181
### Dependency auditing
157
182
158
183
The CI workflow runs `npm audit --audit-level=moderate` on every push and pull request. The build fails if any moderate, high, or critical vulnerabilities are present in the transitive dependency tree. Dependencies are installed with `npm ci` (not `npm install`), which installs exactly what is recorded in `package-lock.json` and fails if there is any discrepancy — preventing lockfile drift and ensuring reproducible installs.
0 commit comments