Skip to content

Commit 17e1b16

Browse files
docs(ADR-010)+fix(spike/#41): record the CORS-mode requirement, emit lint-clean results
Closes #40 alongside the #41 harness work, so one CI round covers both. ADR-010 gained a "measured constraint" section: under `blocked-destinations=(script style)` a same-origin script with a *valid* SHA-256 integrity attribute is still refused unless the request is also made in CORS mode. Measured on Electron 43.2.0 / Chromium while making the #11 exfil probe execute (PR #39). The dangerous part is the failure mode, not the requirement -- refusal is silent, with no CSP-violation event and no console error attributable to the cause, so "did not execute" and "executed and was blocked" share an observable. The packaging work (#24) must therefore emit `crossorigin` alongside the hash; the Open section records that. The harness now emits lint-clean Markdown. Its result documents violated MD007/MD012/MD032/MD036, and docs:lint runs over every .md in the tree -- so the first measurement run that landed in a commit would have turned CI red. Lists are flush-left and preceded by a blank line, the verdict block no longer collapses into consecutive blanks when nothing failed, and the trailing provenance line is a blockquote rather than a whole-line emphasis. Verified: one document from the previous generator produces 7 lint errors (measured by re-linting a retained pre-fix document); the tree now reports 0 issues over 58 files, a count that includes a freshly generated document from a real Electron run. npm test 254 tests / 253 pass / 1 skipped / 0 fail, exit 0; docs:governance passed. That run reproduced HARNESS_EXIT=4 independently of the authoring agent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 5a3c41a commit 17e1b16

2 files changed

Lines changed: 43 additions & 6 deletions

File tree

docs/adr/ADR-010-integrity-policy-same-origin-sri.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,33 @@ unverified one.
7777
hash-and-substitute step, but it is a real build-pipeline requirement that did not
7878
exist before, and it lands on whoever introduces the first script.
7979

80+
**Measured constraint — a correct hash alone is not enough (issue #40).** Observed on
81+
Electron 43.2.0 / Chromium while making the issue #11 exfil probe actually execute
82+
(PR #39): under `blocked-destinations=(script style)` a **same-origin** script carrying
83+
a **valid** SHA-256 `integrity` attribute is still refused. It executes only when the
84+
request is additionally made in CORS mode:
85+
86+
```html
87+
<!-- blocked, despite a valid hash -->
88+
<script src="app.js" integrity="sha256-..."></script>
89+
90+
<!-- executes -->
91+
<script src="app.js" integrity="sha256-..." crossorigin="anonymous"></script>
92+
```
93+
94+
This is a property of the policy adopted here, not of the test fixture. **Every
95+
same-origin script and stylesheet in a future T1 application must therefore be loaded
96+
in CORS mode with integrity metadata**, and anything that generates or packages T1
97+
application HTML must emit both the hash *and* `crossorigin`, keeping the hash in sync
98+
with the bytes it ships.
99+
100+
The dangerous part is the failure mode, not the requirement: refusal is **silent**.
101+
There is no CSP-violation event and no console error attributable to the cause. In the
102+
probe it presented as a payload that simply never ran — which let a downstream
103+
discrimination test pass vacuously, because "did not execute" and "executed and was
104+
blocked" produced the same observable. Any harness relying on this policy must be able
105+
to tell those two apart before it reports a result.
106+
80107
**Rejected alternative — `Integrity-Policy-Report-Only` first.** The usual advice is
81108
to stage a new enforcing header through report-only. It is rejected here because the
82109
app has **zero** scripts and styles today: there is nothing to break, so there is
@@ -103,3 +130,7 @@ the policy is in force — then the ordering advice applies again.
103130
- Whether a future build step emits SRI hashes automatically or fails the build when
104131
an asset lacks one is an implementation decision for the first script-shipping
105132
change, not for this ADR.
133+
- The CORS-mode requirement recorded above lands in the package/verifier work
134+
(issue #24), which is where hash-to-bytes binding is actually implemented. That work
135+
must emit `crossorigin` alongside the hash; emitting only the hash produces assets
136+
that fail silently at runtime.

spike/runtime-eval/harness/electron/main.js

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -581,14 +581,17 @@ const mdCell = lib.mdCell;
581581

582582
function writeResultsMd(s, comparison) {
583583
const now = new Date().toISOString();
584+
// Markdown lists are emitted flush-left and with a blank line before them: the repo's
585+
// docs:lint gate (MD007/MD032) runs over every .md in the tree, so a result document
586+
// that lands in a commit must satisfy it or it turns CI red for a measurement run.
584587
const memLines = Object.entries(s.memByTypeKB)
585-
.map(([t, kb]) => ` - ${t}: ${(kb / 1024).toFixed(1)} MiB (${kb} KB)`).join('\n');
588+
.map(([t, kb]) => `- ${t}: ${(kb / 1024).toFixed(1)} MiB (${kb} KB)`).join('\n');
586589
const probeVerdictLabel = (v) => (v === 'as-expected' ? 'OK' : (v === 'MISSING' ? '**MISSING (not measured)**' : `**${v}**`));
587590
const probeRows = comparison.map((c) =>
588591
`| ${c.index === null ? '—' : c.index} | \`${c.id}\` | ${c.category} | ${c.status} | ${c.expected && c.expected.length ? c.expected.join('/') : '—'} | ${probeVerdictLabel(c.verdict)} | ${mdCell(c.detail)} |`).join('\n');
589592
const blockedRows = s.egress.blockedList.length
590-
? s.egress.blockedList.map((b) => ` - ${b.method} ${b.scheme}: ${b.url} (${b.resourceType})`).join('\n')
591-
: ' - (none — no external request was ever attempted)';
593+
? s.egress.blockedList.map((b) => `- ${b.method} ${b.scheme}: ${b.url} (${b.resourceType})`).join('\n')
594+
: '- (none — no external request was ever attempted)';
592595

593596
// The #status terminal state depends on whether ANY probe FAILed. With the
594597
// cachestorage payload fix in place, a clean run settles at state=done.
@@ -637,8 +640,10 @@ Generated by \`main.js\` from a live run; every number below is straight from th
637640
? '**a control was exercised and FAILED** (see the verdict list below).'
638641
: '**NOT MEASURED** — at least one claimed control was absent, incomplete or inconclusive; this row must not be read as a pass.'}
639642
640-
${s.verdict.failed.length ? s.verdict.failed.map((r) => `- FAILED CONTROL: ${mdCell(r)}`).join('\n') : ''}
641-
${s.verdict.notMeasured.length ? s.verdict.notMeasured.map((r) => `- NOT MEASURED: ${mdCell(r)}`).join('\n') : ''}
643+
${[
644+
...s.verdict.failed.map((r) => `- FAILED CONTROL: ${mdCell(r)}`),
645+
...s.verdict.notMeasured.map((r) => `- NOT MEASURED: ${mdCell(r)}`),
646+
].join('\n') || '- (no failed or unmeasured control)'}
642647
643648
- Run timestamp (wall clock, informational only): ${now}
644649
- Runtime: **Electron ${s.electron}** (Chromium ${s.chromium}, Node ${s.node}, V8 ${s.v8})
@@ -667,6 +672,7 @@ ${s.verdict.notMeasured.length ? s.verdict.notMeasured.map((r) => `- NOT MEASURE
667672
| Local (app://) requests served | ${s.egress.allowedLocal} |
668673
669674
Per-process idle memory (workingSetSize):
675+
670676
${memLines}
671677
672678
${stateNote}
@@ -742,7 +748,7 @@ measured (exit 4). No fixture outcome is prose-only any more.
742748
- ADR-006 stays PROPOSED. This file is one measured row of evidence, not a runtime selection.
743749
744750
---
745-
*ADR-006 Electron harness · isolated spike · ${s.platformLabel} runtime execution ${s.runDate}*
751+
> ADR-006 Electron harness · isolated spike · ${s.platformLabel} runtime execution ${s.runDate}
746752
`;
747753
// #41 P2: 'wx' fails if the file exists. resolveResultsPath already picks an
748754
// unused name; the flag is the second lock so no rerun can ever silently

0 commit comments

Comments
 (0)