Skip to content
Merged
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
15 changes: 15 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
version: 2
updates:
- package-ecosystem: npm
directory: /
schedule:
interval: monthly
groups:
version-updates:
applies-to: version-updates
patterns:
- "*"
security-updates:
applies-to: security-updates
patterns:
- "*"
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ jobs:
cache: npm
- name: Install locked dependencies
run: npm ci
- name: Audit critical vulnerabilities
run: npm audit --audit-level=critical
- name: Type-check
run: npm run check
- name: Unit tests
Expand Down
135 changes: 135 additions & 0 deletions .planning/audits/dependency-security-2026-08-29.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Dependency security residual risk and Astro 7 handoff — 2026-08-29

## Status and scope

Stage 1 reduced the vulnerable dependency graph without crossing the approved
Astro/Cloudflare major-version boundary. The remaining findings are maintenance
risk, not evidence of an active incident. The source conditions described below
make the examined direct advisories currently unreachable or inapplicable, but
those conditions are conditional mitigations, not upstream patches. The project
still needs the separately reviewed Stage 2 migration.

This record was checked against the committed `package-lock.json`, the installed
graph, live npm audit output, repository source, and the linked primary GitHub
Advisory Database records on 2026-08-29.

## Stage 1 evidence

`npm audit` reports vulnerable package keys, not unique advisories. One package
key can aggregate several advisory records and dependency effects.

| Graph | Before Stage 1 | After corrected Stage 1 |
| --- | ---: | ---: |
| Full | 29 keys: 1 low, 12 moderate, 15 high, 1 critical | 10 keys: 0 low, 4 moderate, 6 high, 0 critical |
| `--omit=dev` | 27 keys: 1 low, 12 moderate, 14 high, 0 critical | 10 keys: 0 low, 4 moderate, 6 high, 0 critical |

The corrected full and `--omit=dev` audits now agree because six build-only
packages were reclassified as development dependencies: `@astrojs/check`,
`@tailwindcss/vite`, `@types/react`, `@types/react-dom`, `tailwindcss`, and
`typescript`. That makes the production audit graph meaningful rather than
allowing build tooling to be counted as deployed application dependencies.

Stage 1 removed the Vitest critical path. It also updated Resend and removed the
reported `svix`/`uuid` chain; neither package is present in the current installed
graph or lockfile. The ten residual vulnerable keys are:

- `@astrojs/cloudflare`
- `@astrojs/react`
- `astro`
- `esbuild`
- `miniflare`
- `sharp`
- `undici`
- `vite`
- `wrangler`
- `ws`

Astro 5.18.2 is an interim compatibility update, not remediation of the Astro
advisories. A live standard, non-forced `npm audit fix --dry-run --json` reported
zero packages added, changed, or removed. Further audit remediation therefore
crosses the approved Stage 1 major-version boundaries.

## Dependabot external gate (post-merge)

As verified 2026-08-30, GitHub reports Dependabot security updates (automated
security fixes) disabled for this repository. `.github/dependabot.yml` defines
the reviewed update-grouping policy, but that file cannot enable the separate
repository setting. Advisory-triggered Dependabot updates must not be described
as active while GitHub continues to report the setting disabled.

This is intentionally a post-merge external gate: the default branch must first
contain the reviewed grouping policy before activation. After this branch
reaches the default branch, separately authorize enabling Dependabot security
updates, enable the repository setting, and verify that GitHub reports it as
enabled. This record does not authorize or claim that external change.

## Direct residual advisories

The live audit reports the following direct Astro and Cloudflare-adapter
advisories. "Current condition" is repository-specific reachability evidence,
not a claim that the vulnerable dependency code has been patched locally.

| Advisory | Primary title | Severity | Affected / patched | Current repository condition |
| --- | --- | --- | --- | --- |
| [GHSA-j687-52p2-xcff](https://github.com/advisories/GHSA-j687-52p2-xcff) | Astro: XSS in `define:vars` via incomplete `</script>` tag sanitization | Moderate | Astro `<6.1.6` / `6.1.6` | No `define:vars` directive is used. The `define` key in `astro.config.mjs` is Vite build configuration and is unrelated. |
| [GHSA-xr5h-phrj-8vxv](https://github.com/advisories/GHSA-xr5h-phrj-8vxv) | Astro: Server island encrypted parameters vulnerable to cross-component replay | Low | Astro `<6.1.10` / `6.1.10` | No server islands (`server:defer`) are used. |
| [GHSA-jrpj-wcv7-9fh9](https://github.com/advisories/GHSA-jrpj-wcv7-9fh9) | Astro: XSS via Unescaped Attribute Names in Spread Props | Moderate | Astro `<6.4.6` / `6.4.6` | No spread attributes occur in `.astro` templates. |
| [GHSA-f48w-9m4c-m7f5](https://github.com/advisories/GHSA-f48w-9m4c-m7f5) | Astro: XSS via unescaped spread attribute names in `renderHTMLElement` (incomplete fix for CVE-2026-54298) | Moderate | Astro `<7.0.6` / `7.0.6` | No spread attributes occur in `.astro` templates, so the advisory's untrusted-keyed spread-prop path is absent. |
| [GHSA-7pw4-f3q4-r2p2](https://github.com/advisories/GHSA-7pw4-f3q4-r2p2) | Astro: Cross-site scripting via unescaped `transition:*` directive values on hydrated islands | Low | Astro `>=3.10.0 <7.0.4` / `7.0.4` | No Astro `transition:*` directive is used. |
| [GHSA-4g3v-8h47-v7g6](https://github.com/advisories/GHSA-4g3v-8h47-v7g6) | Astro: Reflected XSS via unescaped View Transition animation properties | Moderate | Astro `>=2.9.0 <=7.0.9` / `7.1.0` | No `ViewTransitions` or `ClientRouter` import is used, and no Astro transition directive is present. |
| [GHSA-2pvr-wf23-7pc7](https://github.com/advisories/GHSA-2pvr-wf23-7pc7) | Astro: Host header SSRF in prerendered error page fetch | High | Astro `<6.4.6` / `6.4.6` | Not reachable through this deployment: the primary advisory explicitly lists `@astrojs/cloudflare` as unaffected because it reads error pages through the ASSETS binding. |
| [GHSA-8hv8-536x-4wqp](https://github.com/advisories/GHSA-8hv8-536x-4wqp) | Astro: Reflected XSS via unescaped slot name | High | Astro `<6.3.3` / `6.3.3` | Current named slots are static: the only name is the literal `icon`; the remaining slots are default slots. No attacker-controlled or dynamic Astro slot name is used. |
| [GHSA-88gm-j2wx-58h6](https://github.com/advisories/GHSA-88gm-j2wx-58h6) | Cloudflare has SSRF via redirect following through its image-binding-transform endpoint (incomplete fix for GHSA-qpr4) | Low | `@astrojs/cloudflare <13.1.10` / `13.1.10` | The required remote-image path is not configured or used: there is no `image.domains`, `image.remotePatterns`, `astro:assets`, Astro `<Image>`, or Astro `<Picture>`. This low-severity advisory does not independently increase urgency under the current configuration. |

The image search also found ordinary verification-image storage and admin-route
text. Those are R2-backed verification uploads, not Astro's remote image service,
and are not evidence that the Cloudflare image advisory is reachable. Likewise,
the only source match for `transition:` is a React inline CSS transition in
`BuildingMap.tsx`; it is not an Astro template directive or View Transition API.

These conclusions must be revisited if the project introduces any construct on
which they rely, changes adapters, or exposes a relevant value to attacker
control. Any such change advances the Stage 2 priority and requires security
review before release.

## Stage 2 migration boundary

Stage 2 is a coordinated, separately reviewed major migration with these package
targets:

- `astro` 7.2.9
- `@astrojs/cloudflare` 14.2.5
- `@astrojs/react` 6.0.4
- a current compatible `@cloudflare/workers-types` (5.20260829.1 was current when
this record was verified)
- `react` and `react-dom` remain at 18.3.1

Astro 7.1.0 is the security floor because it contains the View Transition fix;
7.0.x is not acceptable. The approved Astro 7.2.9 target provides margin above
that floor and matches the Cloudflare adapter's published Astro `^7.2.0` peer
range. Astro 7.2.9 requires Node 22.12.0 or newer.

Implementation must review both official migration guides before changing the
graph:

- [Upgrade to Astro v6 (5→6)](https://docs.astro.build/en/guides/upgrade-to/v6/)
- [Upgrade to Astro v7 (6→7)](https://docs.astro.build/en/guides/upgrade-to/v7/)

After migration 0028, re-audit all of the following against the migrated runtime,
not only against compile-time success:

- Astro `security.checkOrigin`, authenticated and unauthenticated POST routes,
the `application/json` gap, and SameSite cookie behavior;
- SSR behavior; Cloudflare D1 and R2 bindings; authentication; and image routes;
- `getEnv`, `fireAndForget`, `ctx.waitUntil`, fail-closed rate limiting, and
destructive-action audit-log writes.

The release gate is `npm run check`, `npm test`, `npm run build`, and the complete
E2E suite, followed by an immutable exact-SHA preview and explicit preview smoke.
After that passes, verify production-only Turnstile and map behavior plus every
user flow affected by the migration. Preview cannot establish the Turnstile or
map results.

Staying on the current Astro major is therefore operational security work: the
examined fixes were not backported to Astro 5. Current source conditions reduce
exposure, but they do not remove the maintenance obligation or replace Stage 2.
58 changes: 58 additions & 0 deletions docs/superpowers/plans/2026-08-29-dependency-security-refresh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Dependency security refresh implementation plan

**Approved:** 2026-08-29

**Branch:** `codex/dependency-security-refresh`

**Base:** `origin/main` at `a18254a89132ff8ed4da4ef4ae40af3677a12795`

## Goal

Remove the critical dependency advisory and most compatible advisory debt without changing intended product behavior, make dependency risk visible in CI, and leave a verified handoff for the separately reviewed Astro 7 migration.

## Global constraints

- Do not change application features, copy, scoring, database schema or data, Cloudflare settings, environment variables, authentication, moderation, or privacy behavior.
- Do not use `npm audit fix --force`, unsupported dependency overrides, or unreviewed broad rewrites.
- Raise direct dependency floors in `package.json` and commit the reproducible `package-lock.json` resolution.
- Astro 5.18.2 is a compatible interim update, not a complete remediation of Astro advisory debt.
- Stage 2 targets Astro 7.2.9. The security floor is 7.1.0; no 7.0.x version is an acceptable stopping point.
- Keep React and React DOM on 18.3.1 in this stage.
- `main` auto-deploys. Do not push, open a PR, or merge without the user's separate approval gates.
- Preserve the user-owned untracked `src/pages/api/admin/verification/[id] (1).ts` in the root checkout; all work occurs in this isolated worktree.

## Task 1: Pages install authority and pre-change baseline

1. Record the Pages install evidence. Cloudflare's documented default installs all dependencies, but the account API token cannot read project configuration. Treat an exact-SHA Pages preview built after reclassification as the authoritative pre-merge proof that build-time devDependencies are installed.
2. Run a clean `npm ci` from the current lockfile.
3. Record the full and `--omit=dev` audit counts.
4. Run `npm run check`, `npm test`, `npm run build`, and `npm run e2e` before dependency changes. Stop on an unexplained failure.

## Task 2: Compatible dependency refresh

1. Raise direct floors to Astro 5.18.2, Vitest 4.1.11, happy-dom 20.11.13, Resend 6.25.0, and `@astrojs/check` 0.9.10.
2. Move `@astrojs/check`, TypeScript, both React type packages, `@tailwindcss/vite`, and Tailwind CSS from `dependencies` to `devDependencies`.
3. Refresh compatible transitive resolutions without `--force` and review the lockfile changes.
4. Reproduce the expected full audit reduction from 29 to approximately 10, with zero critical findings. Record the actual result rather than forcing it to match the simulation.
5. Run check, unit tests, and build after the dependency refresh.

## Task 3: Dependency guardrails

1. Add a failing workflow-contract test that requires CI to run `npm audit --audit-level=critical` after `npm ci` and before check/test/build.
2. Add the minimal CI workflow change that makes the contract pass.
3. Add `.github/dependabot.yml` for grouped monthly npm version updates and grouped advisory-triggered security updates. Do not describe security updates as weekly; GitHub triggers them from advisories.
4. Add contract coverage for the low-noise Dependabot policy where it protects repository-owned behavior rather than merely mirroring arbitrary YAML text.

## Task 4: Residual advisory and Stage 2 handoff

1. Add a dated planning note that records the actual post-Stage-1 full and production-graph audit counts.
2. Name the residual Astro advisories and the current reachability conditions being relied upon: no attacker-controlled dynamic slots, no remote image allowlist or Astro image components, no transition directives or View Transition imports, and no Astro spread attributes.
3. Record that a change to any relied-upon condition advances the Astro 7 migration priority.
4. Record the approved Stage 2 target and checks: Astro 7.2.9, Cloudflare adapter 14.2.5, React adapter 6.0.4, current compatible Workers types, both migration guides, CSRF re-audit, `getEnv`/`ctx.waitUntil`, fail-closed rate limiting, audit-log writes, D1/R2 bindings, auth, SSR, image route, E2E, immutable preview, and production verification.

## Task 5: Final verification and review

1. Run `npm ci`, both audit views, `npm run check`, `npm test`, `npm run build`, and `npm run e2e` from the committed candidate.
2. Review the complete branch diff, including direct version floors, dependency classification, lockfile, workflows, Dependabot policy, tests, and documentation.
3. Obtain a broad whole-branch review and resolve all load-bearing findings.
4. Present the diff and verification evidence for separate push/PR approval. Do not push during implementation.
9 changes: 5 additions & 4 deletions e2e/admin-actions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,11 @@ test.describe('Review Moderation (E2E-07)', () => {
// Use the second review card to avoid any collision with the approve test card.
// All seed reviews are 'approved' — expand the second card.
const reviewCards = adminPage.locator('.cursor-pointer');
await reviewCards.nth(1).click();
const secondReviewHeader = reviewCards.nth(1);
await secondReviewHeader.click();

// Scope all subsequent actions to the expanded card (second card container)
const secondCard = adminPage.locator('.bg-white.rounded-xl').nth(1);
// Scope all subsequent actions to the expanded card containing that header.
const secondCard = secondReviewHeader.locator('..');

// Click "Reset to Pending" within the second card to expose the Reject button
await secondCard.locator('button', { hasText: 'Reset to Pending' }).click();
Expand All @@ -55,7 +56,7 @@ test.describe('Review Moderation (E2E-07)', () => {
await expect(secondCard.locator('span.rounded-full')).toContainText('pending');

// Click Reject within the same card
await secondCard.locator('button', { hasText: 'Reject' }).click();
await secondCard.locator('button', { hasText: 'Reject' }).first().click();

// Assert status badge within the card updated to 'rejected'
await expect(secondCard.locator('span.rounded-full')).toContainText('rejected');
Expand Down
Loading