From 7473a2fa2787448fed73fa0f7277c1428e003377 Mon Sep 17 00:00:00 2001 From: Michal Ferber Date: Mon, 7 Sep 2026 05:50:56 -0400 Subject: [PATCH] =?UTF-8?q?Point=20the=20=C2=A72=20scan=20at=20the=20publi?= =?UTF-8?q?shed=20surface,=20and=20re-vendor=20the=20script=20that=20could?= =?UTF-8?q?=20not=20aim=20there?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DS §2 (v2.71.0, tgwab-standards#180) makes the eval scan's target the PUBLISHED SURFACE rather than the literal dist/. This repo is the case that produced the ruling. Its `files` list ships BOTH src and dist, and its default export is `"." : "./src/index.js"` — so `import … from 'markdownwizard-tools'` resolves to the SOURCE, and `no-eval.sh dist` covered only the `./iife` subpath. The gate was aimed at the path most consumers do not use. THE SCRIPT IS RE-VENDORED IN THE SAME CHANGE BECAUSE IT HAD TO BE. The copy this repo carried read only "$1", so `./scripts/no-eval.sh dist src` would have scanned dist and dropped src silently — the widened gate would have looked widened and covered exactly what it did before. Demonstrated here, in this repo, with one hazard planted in src/: new script, `dist src` exit 1 src/__negctl__.js:1 ... new Function( OLD script, `dist src` exit 0 "eval-free: OK (dist)" <-- silent miss new script, `dist` exit 0 the hazard really is only in src Removing the control returns the run to green: `eval-free: OK (dist src)`. The output naming both targets is itself the evidence the second is read. NO HAZARD WAS FOUND OR FIXED. src/ scanned clean before this change and scans clean after — all 8 files, exit 0. This is a scope defect, a control aimed at the wrong path, not an incident. Lint and the test suite are unchanged and green. MERGE ORDER: the vendored scripts/no-eval.sh here matches templates/no-eval.sh on tgwab-standards#180, which is open. If that PR changes in review, re-sync this copy before merging — the two are meant to be identical, and #180 adds the suite that proves it. Refs MichalAFerber/tgwab-standards#179 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016beCydw4C9VrgL9eHzGUG2 --- .github/workflows/ci.yml | 20 +++++++++++--------- scripts/no-eval.sh | 34 ++++++++++++++++++++++++++++++---- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0dc1e54..b89892b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -129,17 +129,19 @@ jobs: # the scan is lenient — planting one `new Function(` in dist makes it # exit 1. # - # SCOPE, STATED SO A GREEN RUN IS NOT READ AS MORE THAN IT IS. This + # THE TARGET IS THE PUBLISHED SURFACE, NOT `dist` (DS §2, v2.71.0). This # package's `files` list ships BOTH `src` and `dist`, and its default - # export is `"." : "./src/index.js"` — so a consumer doing - # `import … from 'markdownwizard-tools'` gets the SOURCE, and only the - # `./iife` subpath gets what this scan covers. src/ scans clean today - # (all 8 files, exit 0), so nothing is hiding there; whether §2's target - # should widen for a package whose default export is source is a - # standards question, raised in tgwab-standards#173 rather than decided - # here. Widening this to `dist src` is a one-word change and is green. + # export is `"." : "./src/index.js"` — so a consumer writing + # `import … from 'markdownwizard-tools'` gets the SOURCE, and `dist` + # alone covered only the `./iife` subpath. That was a control aimed at + # the path most consumers do not use. + # + # `scripts/no-eval.sh` is re-vendored in the same change because it HAD + # TO BE: the previous copy read only "$1", so `dist src` would have + # scanned dist and dropped src silently — the widened gate would have + # looked widened and covered exactly what it did before. - name: No runtime code generation in built output (§2) - run: ./scripts/no-eval.sh dist + run: ./scripts/no-eval.sh dist src # Full tree, deliberately not --omit=dev (DS §15): the dev half is the # build toolchain, and what it writes into dist/ is what ships. Excluding it diff --git a/scripts/no-eval.sh b/scripts/no-eval.sh index de07e4e..7d3fff6 100755 --- a/scripts/no-eval.sh +++ b/scripts/no-eval.sh @@ -15,14 +15,40 @@ # The CSP MUST NOT be loosened instead. set -euo pipefail -TARGET="${1:-dist}" +# TARGETS: every path this package actually ships (DS §2). Defaults to `dist`, +# which is what every caller passed before this took more than one. +# +# TWO DEFECTS THIS REPLACED, BOTH OF WHICH REPORTED GREEN OVER NOTHING: +# +# 1. It read only "$1". `no-eval.sh dist src` set TARGET=dist and DROPPED src +# silently — measured 2026-09-07 by planting `new Function(` in src/ and +# running all three forms: `src` alone exits 1, `dist` alone exits 0, and +# `dist src` printed "eval-free: OK (dist)" and exited 0. A gate widened +# that way looks widened, reports green, and scans exactly what it did +# before. +# +# 2. A target that did not exist reported "eval-free: OK" and exited 0, +# because the `|| true` below swallows grep's exit-2. So a build that never +# ran read as a clean scan. Latent rather than live — all 11 adopters run +# this after a build step — but it is the same failure class as (1), and a +# missing target is now a hard error. +if [ "$#" -eq 0 ]; then set -- dist; fi + +missing=() +for t in "$@"; do [ -e "$t" ] || missing+=("$t"); done +if [ "${#missing[@]}" -gt 0 ]; then + printf '::error::no-eval target not found: %s\n' "${missing[*]}" + printf 'A missing target is a failure, not a pass: it used to print "eval-free: OK" and exit 0, so a build that never ran read as a clean scan.\n' + exit 1 +fi + ALLOW=".eval-allowlist" PATTERN='(^|[^A-Za-z0-9_$])eval[[:space:]]*\(|new[[:space:]]+Function[[:space:]]*\(|(^|[^A-Za-z0-9_$.])Function[[:space:]]*\([[:space:]]*["'"'"'`]|set(Timeout|Interval|Immediate)[[:space:]]*\([[:space:]]*["'"'"'`]' hits="$(grep -nEr "$PATTERN" \ --include='*.js' --include='*.mjs' --include='*.cjs' --include='*.html' \ - "$TARGET" || true)" + "$@" || true)" if [ -s "$ALLOW" ]; then hits="$(printf '%s\n' "$hits" \ @@ -30,9 +56,9 @@ if [ -s "$ALLOW" ]; then fi if [ -n "$hits" ]; then - printf '::error::runtime code-generation found in %s (CSP has no %s)\n' "$TARGET" "'unsafe-eval'" + printf '::error::runtime code-generation found in %s (CSP has no %s)\n' "$*" "'unsafe-eval'" printf '%s\n' "$hits" exit 1 fi -printf 'eval-free: OK (%s)\n' "$TARGET" +printf 'eval-free: OK (%s)\n' "$*"