# Security policy

## Report a vulnerability

Email **kay@pkbcreates.com** with the subject **“Security report”**. Include the affected URL, a short reproduction, and the impact. Please do not publish an exploitable issue before it has been acknowledged.

This is a static portfolio site. It has no server, database, payment flow or user accounts. The privileged surface is the Decap CMS at `/admin/`, which writes to GitHub through a GitHub OAuth Worker.

## Protections in this repository

- **No secrets in the frontend.** `admin/config.yml` contains only the repository name, branch and public OAuth Worker URL. The GitHub client secret belongs in the Worker’s encrypted environment variables, never in this repo.
- **Content Security Policy.** Public pages use a strict policy: no inline executable scripts, scripts only from this origin and the pinned GSAP CDN, frames only from YouTube’s no-cookie player. The admin has a separate policy because Decap 3.6 needs `eval` for its schema validator.
- **Subresource Integrity.** GSAP, ScrollTrigger and Decap CMS are pinned to exact versions and loaded with SHA-384 integrity plus CORS. If a CDN file changes, the browser refuses it.
- **Unused identity widget removed.** The old Netlify Identity script was not part of the active GitHub backend and has been removed.
- **Security headers.** Public pages carry a strict CSP in both their `<meta>` tags and matching HTTP headers; `_headers` also adds HSTS, clickjacking protection, MIME sniffing protection, a permissions policy, a strict referrer policy and the separate admin CSP. The admin is `no-store` and `noindex`.
- **No third-party analytics, pixels or tracking scripts.**
- **CMS uploads are treated as untrusted.** `_headers` sends a script-blocking CSP and `nosniff` for `/assets/images/uploads/*`, so a rogue SVG or HTML upload cannot execute when opened directly. Upload only trusted raster images or safe SVGs.
- **Escaped CMS output.** `assets/js/cms-loader.js` escapes text and validates link targets before rendering CMS entries.
- **Safe external links.** `target="_blank"` links use `rel="noopener noreferrer"`.

## Deployment checklist — required

These steps cannot be done inside the static files. They are the difference between “hardened markup” and an actually protected admin.

1. **Put Cloudflare Access in front of `/admin*`.** Create a Zero Trust application for the admin path and require your email/SSO. This is the single strongest protection for the CMS.
2. **Protect the GitHub account.** Enable 2FA, use a strong unique password, and keep the account and organisation owner list small.
3. **Restrict the OAuth Worker.** The ready-to-deploy Worker is in `oauth-worker/` with its own README. The Worker must:
   - keep `GITHUB_CLIENT_SECRET` in encrypted environment variables;
   - validate the OAuth `state` value and reject mismatched redirects;
   - allow only `https://pkbcreates.com` as its CORS/return origin;
   - never log access tokens or put them in URLs beyond the standard OAuth flow;
   - use the exact GitHub OAuth callback URL.
4. **Use the exact Worker origin.** Replace `https://*.workers.dev` in `_headers` and the admin meta CSP with the real Worker hostname once it exists. The wildcard is a deliberate temporary allowance for the placeholder.
5. **Protect `main`.** Require pull requests and review before merging. Limit who can use the CMS; every CMS save is a Git commit.
6. **Keep dependencies current.** If GSAP, ScrollTrigger or Decap is upgraded, update its version, SHA-384 integrity hash and CSP source together.
7. **Back up content.** Keep an export of `content/`, `assets/images/uploads/` and the PDFs outside the repository.
8. **Watch the logs.** Review GitHub audit logs and Cloudflare Access logs periodically for unexpected admin sign-ins.

## If you add a third-party service

Update all of the following together:

- the relevant `script-src`, `connect-src`, `img-src` or `frame-src` directive in `_headers` and in the page meta CSP;
- the subresource integrity hash for any new script;
- the privacy/colophon wording if visitor data leaves the browser.

Do not add a service that requires a secret in frontend JavaScript. There is no safe way to hide a secret in static HTML.
