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
12 changes: 12 additions & 0 deletions .github/audit-exceptions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"exceptions": [
{
"id": "GHSA-vfj7-8cjw-p6xm",
"package": "braces",
"approvedBy": "writingdeveloper",
"approvedOn": "2026-10-22",
"expires": "2026-12-31",
"review": "docs/reviews/2026-10-22-braces-dev-audit-exception.md"
}
]
}
10 changes: 9 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,13 @@ jobs:
- name: Build
run: npm run build

# Always check production independently so a dev-tool advisory cannot
# conceal any newly introduced runtime vulnerability.
- name: Audit production dependencies
run: npm audit --omit=dev --audit-level=high

# Same threshold over every dependency. An advisory passes only when
# .github/audit-exceptions.json lists it with an expiry and a review, and
# every installed copy is dev-only; each pass is annotated on the run.
- name: Audit dependencies
run: npm audit --audit-level=high
run: npm run audit:deps
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,15 @@ npm run format:check
npm test
npm run untranslated
npm run verify:hero -- public/images/posts/*.webp
npm audit --audit-level=high
npm audit --omit=dev --audit-level=high
npm run audit:deps
npm run build
```

`npm run format` applies the repository's Prettier rules to TypeScript/TSX source. The formatter accepts either LF or CRLF line endings so the check behaves the same on Windows and Linux.

The production audit takes no exceptions. `npm run audit:deps` audits every dependency at the same high threshold and lets an advisory through only when [`.github/audit-exceptions.json`](.github/audit-exceptions.json) lists it with an expiry and a review record, and every installed copy is a dev dependency.

## Content model

```text
Expand Down
48 changes: 48 additions & 0 deletions docs/reviews/2026-10-13-keystatic-chokidar-runtime-isolation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Security review — Keystatic watcher dependency isolation (2026-10-13 PT)

Source: isolated 4080 MAIN worktree `portfolio-security-chokidar4-20261013` branched from security PR #33 / `bbd749b`. No production deployment or risk exception is approved.

## What changed

Keystatic's latest Next adapter (`@keystatic/next@5.0.5`) declares `chokidar@^3.5.3`, bringing `braces@3.0.3` into the **installed production dependency graph**. npm reports GHSA-vfj7-8cjw-p6xm (high, stack-exhaustion via deeply nested brace patterns), with no patched upstream braces release as of Oct 13.

An explicitly scoped npm override now resolves **only** `@keystatic/next → chokidar` to Chokidar **4.0.3**. Chokidar 4 does not depend on `braces`. Both package lock and independently run audits confirm `braces@3.0.3` exists *only* in development, pulled by `eslint-config-next → @next/eslint-plugin-next → fast-glob → micromatch`.

This is a real reduction in production package exposure, **not a vulnerability exception or silent suppression**:
- `npm audit --omit=dev --audit-level=high`: **0 vulnerabilities / exit 0** after the override, previously three High from Keystatic/Chokidar.
- `npm audit --audit-level=high`: **FAIL, five High** in development tools because `braces@3.0.3` is still present for ESLint. No compatible official patched release or replacement was validated.
- GitHub CI now has two independent gates: **Audit production dependencies** and the existing unchanged **Audit dependencies**. The strict full-audit gate remains red until a real fix or separately and explicitly reviewed time-bounded policy.

## Compatibility observations

The override crosses a declared semver-major boundary (`^3.5.3` → 4.0.3). It is therefore treated as a reviewed compatibility experiment, **not** claimed to have Keystatic vendor support.

Source inspection of `@keystatic/next/dist/keystatic-next-api.js` finds its lazy `require('chokidar')` inside a **development-only** legacy reader-refresh watcher path. The legacy call is `watch(directories, {ignored: [/node_modules/]})`, not a glob pattern. The active Next App Router route uses `makeRouteHandler`, not this legacy Node/Pages adapter.

Observed tests on the isolated worktree with clean `npm ci`:
- npm lockfile assertions for Chokidar 4 and `braces.dev=true`, and production-disabled Keystatic guard: four added regression tests PASS.
- TypeScript, ESLint, Prettier, total **201/201 unit tests** and Next.js 16.4.0 optimized production build: PASS.
- The exact Keystatic-compatible watcher call with a temporary directory and a real new file: `ready` + `add` events PASS; the temporary directory was removed.
- Local **development-mode** `http://localhost:3199/keystatic`: HTTP 200, real dashboard loaded with 10 Korean + 10 English post entries and project/category controls, no JavaScript page errors.
- **Production-mode** `http://localhost:3198/keystatic`, `/api/keystatic/posts`, `/api/keystatic/reader-refresh/test`: all HTTP 404 with no GitHub storage configured. Home and English home HTTP 200, `/api/og?title=QA` HTTP 200 PNG.

Limitations: a full CMS create/edit/save workflow was **not** exercised (would mutate test content); no authenticated GitHub-storage OAuth mode was tested. These remain separate acceptances if the CMS is to be enabled in production. Do not infer that all independent `braces` uses are absent: it is still in developer tooling.

## Existing production baseline and release decisions

GitHub Dependabot `#82` flagged Next.js 16.3.3 Critical ImageResponse RCE; other default branch alerts `#84/#85/#86` concern `brace-expansion`. Draft security PR #33's Next.js 16.4.0 + transitive patches address those older vulnerable versions, but remain unmerged.

The public site currently uses `ImageResponse` and user-supplied title/description as **div text** under 120/200-character clamps, rather than directly supplying attacker-controlled SVG markup/attributes. The critical advisory specifies that SVG-controlled inputs are affected. This suggests a narrower exposure but is **not** proof of safety across all Next.js advisories or reasons to leave its version outdated.

Official references:
- https://github.com/advisories/GHSA-vfj7-8cjw-p6xm
- https://github.com/vercel/next.js/security/advisories/GHSA-vcvr-r3jv-pc5j

## Remaining gates

1. Keep PR #33 draft until Linux CI and review results are recorded; keep the strict full dependency audit red.
2. Review any separately proposed temporary dev-only GHSA-vfj7-8cjw-p6xm exception with explicit identity, expiry, approver, mitigation and continuous visible audit. **This document is NOT that approval**.
3. If no exception is authorized, investigate a compatible lint dependency replacement or wait for upstream advisory/fix, without global audit bypasses, and do not merge.
4. After security PR passes/gets approved, rebase/merge its changes into staged portfolio PR #34; resolve overlapping three PDF files/CTAs with separate draft PR #37 through explicit editorial approval; verify live Vercel routes, PDF SHA, both locales, canary and rollback in FOLIO-36.

Plane source of truth: FOLIO-45 security / FOLIO-36 release / FOLIO-51 résumé availability.
51 changes: 51 additions & 0 deletions docs/reviews/2026-10-22-braces-dev-audit-exception.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Security review — dated dev-only exception for GHSA-vfj7-8cjw-p6xm (2026-10-22 PT)

Status: **approved exception**. Approver: repository owner (`writingdeveloper`), in the 2026-10-22 PT working session, choosing option A of the three presented (dated dev-only exception / keep CI red until upstream fixes / remove the Next.js ESLint rules). Expires **2026-12-31** inclusive (UTC). From 2027-01-01 the full audit fails again unless the entry is removed or renewed through a new review.

This is the separately reviewed, time-bounded policy that the [2026-10-13 Keystatic isolation review](2026-10-13-keystatic-chokidar-runtime-isolation.md) required before PR #33 could merge.

## Identity

- Advisory: [GHSA-vfj7-8cjw-p6xm](https://github.com/advisories/GHSA-vfj7-8cjw-p6xm) / CVE-2026-93687, high (CVSS 7.5, CWE-674), published 2026-09-18, last updated 2026-10-02.
- Affected: `braces` <= 3.0.3. No patched release exists: 3.0.3 is the latest published version on 2026-10-22.
- Impact: deeply nested brace patterns under the length limit exhaust the call stack and terminate the Node.js process with an uncaught `RangeError`. Denial of service only; no code execution or data exposure is described.
- Installed copy: `node_modules/braces` 3.0.3, marked `dev: true` in package-lock.json.
- Only path: `eslint-config-next` 16.4.2 → `@next/eslint-plugin-next` → `fast-glob` → `micromatch` → `braces`. No current `eslint-config-next` release avoids it; npm's only suggested fix is a semver-major downgrade to 14.2.35.

## Why the risk is acceptable for a bounded period

- `braces` is not in the production dependency graph. The 2026-10-13 Keystatic watcher override removed the runtime copy, and `npm audit --omit=dev --audit-level=high` reports 0 vulnerabilities. That CI step is unchanged and takes no exceptions.
- The copy runs only inside ESLint: local development and the CI lint step. Its inputs are this repository's lint configuration and file globs, not request data. `next build` on Next.js 16 does not run ESLint, so the Vercel build does not load it either.
- Exploiting it requires committing a crafted glob to the repository, and the result is a crashed lint run.
- Keeping CI red is worse: every pull request stays blocked while production remains on Next.js 16.3.3, which is affected by the 2026-10-22 critical Next.js advisories that PR #33 fixes.

## Enforcement

`.github/audit-exceptions.json` records the advisory, package, approver, approval date, expiry and this review. CI's **Audit dependencies** step runs `npm run audit:deps` (`scripts/audit-deps.ts`, logic in `src/lib/audit-gate.ts`, tests in `src/lib/__tests__/audit-gate.test.ts`), which applies the `--audit-level=high` threshold to the full report and fails when:

- any high or critical advisory is not listed;
- a listed advisory has an installed copy that is not `dev: true` (including `devOptional` or a copy missing from the lockfile);
- the listed package differs from the advisory's package;
- the exception has expired, is malformed, or runs longer than 90 days from its approval date;
- `npm audit` returns no report (for example, the registry is unreachable).

Every run prints each excepted advisory as a GitHub Actions warning annotation, and flags an entry that no longer matches any advisory so it can be removed. The 2026-10-13 regression test separately keeps every installed `braces` copy dev-only.

## Verification on 2026-10-22 PT

- `npm run audit:deps`: 0 blocking, 1 excepted (`node_modules/braces`).
- Negative runs, each exit 1: an expired entry (`expired`), an empty exception list (`no-exception`), and an unreachable registry (`audit-error`).
- 21 unit tests cover expiry (inclusive), dev-only checks, per-advisory matching, the severity threshold, stale entries, malformed entries and audit errors.
- GitHub CI evidence is recorded on PR #33 and in Plane FOLIO-45.

## Removal and renewal

- Remove the entry when a fixed `braces` is published or the ESLint chain drops it. The gate warns when the entry no longer matches.
- Renewal needs a new review in `docs/reviews`, a new approval date and an expiry no more than 90 days later.

## Limitations

- The exception accepts a developer-tool denial-of-service risk for a bounded period. It does not make `braces` safe.
- Two moderate `@vitest/mocker` advisories (GHSA-qxg6-89mg-rqv8, GHSA-xp26-3454-mjgp) sit below the threshold and are not covered here. They need a separate `vitest` update.

Plane source of truth: FOLIO-45.
Loading
Loading