Skip to content

Commit 5e6af91

Browse files
authored
fix(security): hardens auth, XSS, rate limiting, and repo posture (#43)
* fix(security): hardens auth, XSS, rate limiting, and repo posture Auth lifecycle: - clears dashboard cache on token expiry (prevents data leak to next user) - propagates 401 from allSettled to trigger re-auth (prevents zombie polling) - adds cross-tab auth sync via storage event listener - moves OAuth returnTo read to after CSRF check passes XSS / injection: - validates pr.htmlUrl with isSafeGitHubUrl before passing to StatusDot/SizeBadge - replaces inline style label colors with adoptedStyleSheets registry - removes style-src-attr unsafe-inline from CSP (zero unsafe-* directives) - refactors UserAvatarBadge inline style to Tailwind class Rate limiting / DoS: - caps fetchRepos pagination at 1000 repos with warning notification - serializes discoverUpstreamRepos to prevent TOCTOU race on repo cap - adds length bounds to VALID_REPO_NAME regex (consistent with Zod schema) - adds soft warning at 100+ repos in RepoSelector - adds in-memory rate limiter to Worker token exchange endpoint - adds HSTS to Worker SECURITY_HEADERS (matches static asset headers) Stores / cache: - adds .max(500) to ignoredItems with FIFO eviction - adds pruneClosedIgnoredItems (30-day TTL) called on app startup - wraps IndexedDB QuotaExceededError retry in try/catch (logs instead of crash) Repo posture: - adds SECURITY.md with responsible disclosure policy - adds CODEOWNERS - adds .npmrc with ignore-scripts=true - pins happy-dom to exact version (removes caret range) - adds CSP inline script hash verification to CI - adds Sentry security token support to Worker tunnel - documents Worker _dsnCache safety rationale - fixes poll coordinator mount/unmount race * fix(security): addresses quality gate findings from domain review * fix(security): restores style-src-attr (corvu/kobalte need it), uses expireToken for cross-tab sync * fix(security): addresses PR review findings with tests - removes IP from rate-limit log, adds prune threshold guard - makes SENTRY_DSN optional in Env interface - moves returnTo consumption after auth success in OAuthCallback - adds race-safe coordinator guard to onAuthCleared + clearHotSets - extracts IGNORED_ITEMS_CAP constant, fixes FALLBACK_FG consistency - documents CSP style-src-attr requirement (Kobalte) in SECURITY.md - adds 13 tests: cross-tab auth sync, 401 propagation, label-colors CSS rules, fetchRepos cap, rate-limit window reset, multi-user upstream discovery cap * fix(security): removes dead script, adds remaining test coverage - deletes orphaned scripts/verify-csp-hash.mjs (replaced by inline CI shell) - updates prek.toml and deploy.yml to use inline CSP hash check - adds tests for returnTo preserved on validateToken failure and token exchange failure - adds test for SENTRY_DSN: undefined (optional field coverage) * fix(deps): regenerates lockfile for pinned happy-dom, adds lockfile sync hook - pnpm-lock.yaml specifier was ^20.8.9, package.json was pinned to 20.8.9 — CI frozen-lockfile install failed on the mismatch - adds lockfile-sync pre-commit hook that runs pnpm install --frozen-lockfile when package.json or pnpm-lock.yaml changes * refactor(security): extracts CSP hash verification into shared script * perf(waf): parallelizes smoke tests with GNU parallel Replaces sequential curl loops (81s) with GNU parallel (-j10, ~2s). Test specs become a pipe-delimited data array parsed by exported functions. Adds --max-time 10 per curl, --timeout 15 per job, command -v guard, and empty-output infrastructure failure detection.
1 parent 046b511 commit 5e6af91

35 files changed

+850
-198
lines changed

.github/CODEOWNERS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Default owner for everything
2+
* @wgordon17

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ jobs:
1414
node-version: 24
1515
cache: pnpm
1616
- run: pnpm install --frozen-lockfile
17+
- name: Verify CSP inline script hash
18+
run: bash scripts/verify-csp-hash.sh
1719
- run: pnpm run typecheck
1820
- run: pnpm test
19-
- name: Verify CSP hash
20-
run: node scripts/verify-csp-hash.mjs
2121
- name: Install Playwright browsers
2222
run: npx playwright install chromium --with-deps
2323
- name: Run E2E tests

.github/workflows/deploy.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ jobs:
1818
- run: pnpm install --frozen-lockfile
1919
- run: pnpm run typecheck
2020
- run: pnpm test
21-
- name: Verify CSP hash
22-
run: node scripts/verify-csp-hash.mjs
21+
- name: Verify CSP inline script hash
22+
run: bash scripts/verify-csp-hash.sh
2323
- name: WAF smoke tests
2424
run: pnpm test:waf
2525
- run: pnpm run build

.npmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ignore-scripts=true

SECURITY.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Security Policy
2+
3+
## Reporting a Vulnerability
4+
5+
If you discover a security vulnerability in this project, please report it responsibly.
6+
7+
**Do not open a public GitHub issue for security vulnerabilities.**
8+
9+
Instead, please email **security@gordoncode.dev** with:
10+
11+
- A description of the vulnerability
12+
- Steps to reproduce
13+
- Any relevant logs or screenshots
14+
- Your assessment of severity (if applicable)
15+
16+
You should receive an acknowledgment within 48 hours. I will work with you to understand and address the issue before any public disclosure.
17+
18+
## Scope
19+
20+
This policy covers:
21+
22+
- The github-tracker web application (`gh.gordoncode.dev`)
23+
- The Cloudflare Worker backend (`src/worker/`)
24+
- The OAuth authentication flow
25+
- Client-side data handling and storage
26+
27+
## Out of Scope
28+
29+
- Vulnerabilities in third-party dependencies (report these to the upstream project)
30+
- Issues requiring physical access to a user's device
31+
- Social engineering attacks
32+
- Denial of service attacks against GitHub's API (rate limits are GitHub's domain)
33+
34+
## Security Controls
35+
36+
This project implements the following security measures:
37+
38+
- **CSP**: Strict Content-Security-Policy with `default-src 'none'`, no `unsafe-eval`, no `unsafe-inline` for scripts (`style-src-attr 'unsafe-inline'` required by Kobalte UI library)
39+
- **CORS**: Strict origin equality matching on the Worker
40+
- **OAuth CSRF**: Cryptographically random state parameter with single-use enforcement
41+
- **Read-only API access**: Octokit hook blocks all write operations
42+
- **Input validation**: GraphQL query parameters validated against allowlisted patterns
43+
- **Sentry PII scrubbing**: Error reports strip auth tokens, headers, cookies, and user identity
44+
- **SHA-pinned CI**: All GitHub Actions pinned to full commit SHAs

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
"@testing-library/user-event": "14.6.1",
3838
"daisyui": "5.5.19",
3939
"fake-indexeddb": "6.2.5",
40-
"happy-dom": "^20.8.9",
40+
"happy-dom": "20.8.9",
4141
"tailwindcss": "4.2.2",
4242
"typescript": "5.9.3",
4343
"vite": "8.0.1",

pnpm-lock.yaml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

prek.toml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@ hooks = [
1313
[[repos]]
1414
repo = "local"
1515

16+
[[repos.hooks]]
17+
id = "lockfile-sync"
18+
name = "Lockfile in sync with package.json"
19+
language = "system"
20+
entry = "pnpm install --frozen-lockfile"
21+
pass_filenames = false
22+
files = "(package\\.json|pnpm-lock\\.yaml)$"
23+
priority = 0
24+
1625
[[repos.hooks]]
1726
id = "typecheck"
1827
name = "TypeScript typecheck"
@@ -35,7 +44,7 @@ priority = 0
3544
id = "csp-hash"
3645
name = "CSP hash verification"
3746
language = "system"
38-
entry = "node scripts/verify-csp-hash.mjs"
47+
entry = "bash scripts/verify-csp-hash.sh"
3948
pass_filenames = false
4049
always_run = true
4150
priority = 0

scripts/verify-csp-hash.mjs

Lines changed: 0 additions & 52 deletions
This file was deleted.

scripts/verify-csp-hash.sh

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
SCRIPT=$(sed -n 's/.*<script>\([^<]*\)<\/script>.*/\1/p' index.html)
5+
if [[ -z "$SCRIPT" ]]; then
6+
echo "No inline <script>...</script> found in index.html"
7+
[[ -n "${GITHUB_ACTIONS:-}" ]] && echo "::error::No inline <script>...</script> found in index.html"
8+
exit 1
9+
fi
10+
HASH=$(printf '%s' "$SCRIPT" | openssl dgst -sha256 -binary | base64)
11+
12+
if ! grep -qF "sha256-$HASH" public/_headers; then
13+
echo "CSP hash mismatch! Inline script hash 'sha256-$HASH' not found in public/_headers"
14+
echo "Update the sha256 hash in public/_headers to match the inline script in index.html"
15+
[[ -n "${GITHUB_ACTIONS:-}" ]] && echo "::error::CSP hash mismatch! Inline script hash 'sha256-$HASH' not found in public/_headers"
16+
exit 1
17+
fi
18+
19+
echo "CSP hash verified: sha256-$HASH"

0 commit comments

Comments
 (0)