create-website-build-kit 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-website-build-kit",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Scaffold a production marketing site \u2014 Astro on Cloudflare Workers, with the gates, the migration playbook and the accessibility work already wired.",
5
5
  "keywords": [
6
6
  "astro",
@@ -109,6 +109,13 @@ One shipped as body copy on a service page — *"⚠ CONFIRM: the old site adver
109
109
  Saturday at 9am…"* — past clean types, clean axe and clean tells. **The question is usually real:
110
110
  move it to `BUILD-STATE.md`, do not just delete it.**
111
111
 
112
+ **`npm audit` reports a high, and it cannot be fixed.** One advisory, in accessibility *testing*
113
+ tooling — `pa11y-ci → puppeteer → @puppeteer/browsers → extract-zip` — with **no patched version
114
+ published**. Production dependencies report **zero**, nothing under `src/` imports it, and a
115
+ production install does not pull it. `docs/dependencies.md` carries the chain, the three reasons
116
+ it cannot be fixed here, and the wording to give a client who asks. Do not answer that question
117
+ from memory, and never claim the site has "no vulnerabilities".
118
+
112
119
  **Evidence, not assertions.** `npm run a11y:evidence` writes a dated pack to
113
120
  `docs/a11y-evidence/` — commit it. It fills in the machine half and leaves the keyboard,
114
121
  screen-reader and forms passes **blank**, because no tool does those; a pack with them still
@@ -0,0 +1,102 @@
1
+ # Why `npm audit` reports a high, and why it is not fixed
2
+
3
+ Run `npm audit` on this site and it reports **high-severity findings that nothing can fix
4
+ today**. That is expected, it is documented here rather than left to be rediscovered, and the
5
+ answer to give a client who asks is at the bottom.
6
+
7
+ Check it yourself before reading further:
8
+
9
+ ```bash
10
+ npm audit --omit=dev # production dependencies only
11
+ npm audit # everything, including build tooling
12
+ ```
13
+
14
+ ## The short version
15
+
16
+ | | |
17
+ | --- | --- |
18
+ | Production dependencies | **0 findings** — nothing that reaches a built page is affected |
19
+ | Development tooling | 1 advisory, reported by `npm audit` as several rows |
20
+ | Advisory | [GHSA-jmr9-qjv8-65gv] — `extract-zip` unvalidated symlink path traversal, CVSS **8.1** |
21
+ | Patched version | **none exists** |
22
+ | Reachable from the site | **no** — nothing under `src/` imports it |
23
+
24
+ ⚠ **`npm audit` and GitHub disagree on the count, and both are right.** `npm audit` prints one
25
+ row per affected package in the chain; GitHub's Dependabot prints one per *advisory*. Six rows,
26
+ one problem. Do not read the larger number as six separate holes.
27
+
28
+ ## Where it comes from
29
+
30
+ Accessibility testing is the whole chain. Each step declares the next:
31
+
32
+ ```
33
+ pa11y-ci → pa11y ^9.1.1 → puppeteer ^24.37.5 → @puppeteer/browsers 2.13.2 → extract-zip ^2.0.1
34
+ ```
35
+
36
+ `extract-zip` is what unpacks the Chrome build that puppeteer downloads the first time you run
37
+ `npm run a11y`.
38
+
39
+ ## Why it cannot be fixed here
40
+
41
+ Three reasons, each checkable:
42
+
43
+ 1. **There is no patched release, and no version to override to.** The advisory's
44
+ `first_patched_version` is empty — but that is the advisory's word for it, so check the
45
+ registry instead: `npm view extract-zip versions` ends at **2.0.1**, which *is* the vulnerable
46
+ version. Nothing above it has ever been published, and the last release was **2023-03-04**.
47
+ An `overrides` entry in `package.json` is the usual escape hatch for a transitive pin; here
48
+ there is nothing to point it at.
49
+ 2. **`npm audit fix` changes nothing.** It reports `+0 ~0 -0 packages`. Confirm with
50
+ `npm audit fix --dry-run`.
51
+ 3. **`puppeteer` pins `@puppeteer/browsers` at an exact `2.13.2`**, not a range. Even forcing a
52
+ resolution cannot move it without replacing puppeteer itself, and `pa11y-ci` is already at
53
+ its latest release.
54
+
55
+ The fix has to come from upstream. Until it does, the honest position is a documented known,
56
+ not a silent one.
57
+
58
+ ## Why the exposure is not what the score suggests
59
+
60
+ **CVSS 8.1 describes the vulnerability, not our exposure to it.** Exploiting it means feeding
61
+ the extractor a malicious archive — in practice, a compromised Chrome download. That requires
62
+ an attacker who already controls the archive puppeteer fetches, and the vector is marked as
63
+ requiring user interaction.
64
+
65
+ More to the point:
66
+
67
+ - It is a **development** dependency. It is not bundled, imported, or served.
68
+ - **Nothing under `src/` references pa11y or puppeteer.** Only three scripts do —
69
+ `check-a11y.mjs`, `a11y-evidence.mjs` and `md-to-pdf.mjs` — and none of them run in a
70
+ deploy.
71
+ - A production install (`npm ci --omit=dev`) does not install it at all.
72
+
73
+ ⚠ **This is a reason it is low risk, not a reason it is fine.** If you ever start running the
74
+ a11y scripts on untrusted input, or in CI with credentials worth stealing, re-evaluate rather
75
+ than re-reading this paragraph.
76
+
77
+ ## What to tell a client who asks
78
+
79
+ > The audit finding is in accessibility *testing* tooling, not in the website. It is not part
80
+ > of what gets deployed, and the live site does not contain it. There is currently no fixed
81
+ > version published by its maintainers; when there is, it will be picked up on the next
82
+ > dependency update. The site's production dependencies report no findings.
83
+
84
+ Every clause there is checkable with the two commands at the top. Do not soften it further, and
85
+ do not claim the site is "not affected by any vulnerabilities" — that is a bigger claim than the
86
+ evidence supports, and it is the kind of sentence that gets quoted back.
87
+
88
+ ## When this changes
89
+
90
+ Re-check whenever `pa11y-ci` releases, and whenever an audit shows a *different* advisory —
91
+ this note covers exactly one, and a second finding is a new decision, not this one repeating.
92
+
93
+ ```bash
94
+ npm audit --omit=dev # must stay at 0. If it is not, that is a real problem
95
+ npm outdated pa11y-ci # the chain moves when this does
96
+ npm view extract-zip versions # the day something above 2.0.1 appears, this note is obsolete
97
+ ```
98
+
99
+ If a patched `extract-zip` ships and the chain picks it up, delete this file. A note describing
100
+ a problem that no longer exists is worse than no note.
101
+
102
+ [GHSA-jmr9-qjv8-65gv]: https://github.com/advisories/GHSA-jmr9-qjv8-65gv
@@ -177,9 +177,20 @@ async function req(url, options = {}) {
177
177
  const controller = new AbortController();
178
178
  const timer = setTimeout(() => controller.abort(), 20000);
179
179
  const follow = options.redirect !== 'manual';
180
+ /*
181
+ * ⚠ 20, BECAUSE THAT IS WHAT fetch ITSELF ALLOWS. Following redirects by
182
+ * hand is what lets every hop be checked, but the hop LIMIT is not a
183
+ * security property — and picking a lower one silently changes results.
184
+ *
185
+ * At a cap of 5, a page behind a longer chain came back as the 302 rather
186
+ * than the 200 it used to: measured, old → 200, new → 302. In a migration
187
+ * inventory that turns a live page into a redirect, which is the kind of
188
+ * wrong that reads as fine.
189
+ */
190
+ const MAX_HOPS = 20;
180
191
  try {
181
192
  let res = await fetch(url, { ...options, redirect: 'manual', signal: controller.signal });
182
- for (let hops = 0; follow && res.status >= 300 && res.status < 400 && res.headers.get('location') && hops < 5; hops++) {
193
+ for (let hops = 0; follow && res.status >= 300 && res.status < 400 && res.headers.get('location') && hops < MAX_HOPS; hops++) {
183
194
  const next = new URL(res.headers.get('location'), url).toString();
184
195
  const hopRefusal = blockedReason(next, { allowInternal });
185
196
  if (hopRefusal) return refuse(`${hopRefusal.reason} — reached by a redirect from ${url}`, next);