Skip to content

test(site): run the shipped site.js in CI; document the local-token boundary - #10

Merged
DevCop95 merged 2 commits into
mainfrom
test/site-js-and-boundary
Oct 18, 2026
Merged

DevCop95 merged 2 commits into
mainfrom
test/site-js-and-boundary

Conversation

@DevCop95

Copy link
Copy Markdown
Owner

Two things the session found by doing rather than reading.

test(site): the shipped site.js now runs in CI

docs/site.js is the file GitHub Pages serves, and nothing could see it. The
moment it was executed instead of read, it turned out the URL check was a
startsWith on the raw string, so

.../releases/download/v9/../../../evil   ->   https://github.com/DevCop95/evil

survived it. That was already fixed in #9 — but without a test, the next edit
could undo it and nothing would notice. A wrong API field name or a renamed
element id in index.html fails the same way: empty download table, no error
anywhere.

docs/site.test.js runs the real file against a minimal DOM stub. No
dependencies, no network — fixtures are trimmed copies of a real release
response, so the gate cannot go red because GitHub was slow.

Scenario What it proves
A the real 7-asset shape fills every column, SHA-256 matches checksums.txt, em dash where it should not
B javascript:, foreign host and ../ traversal all dropped; a raw HTML asset name lands in textContent, not markup
C/D nothing usable / a 403 both fall back to the pinned list and name the real reason
E checksums.txt unreachable: table still renders, hashless
F the hand-written fallback table in index.html still matches the published hashes and byte counts

The element ids are read from index.html, so renaming one there fails
here instead of silently emptying the page. A static check fails if any code
path starts building markup from strings.

Verified under node 20 (what the job pins) and node 24: 24 assertions, all
passing. New required check: site js test.

docs(security): the two boundaries #6 asked to be written down

#6 was never a bug — it was two boundaries the code states and the docs did
not. Both now live in SECURITY.md under Documented limits:

  1. The token is a lock on the door, not on the room. GET / hands it to
    the page and cannot tell a browser from any other local process with
    loopback access. Both deployment cases spelled out: on a single-user
    workstation it adds no exposure beyond what a same-user process already has;
    on a shared or RDP host, an unprivileged process that reads it can drive an
    elevated --enable-collect server. The fix (named pipe, SID ACL) and the
    rejected options are named.
  2. POST /v1/sys/opt changes the machine with no flag. Documented rather
    than gated, because gating it would change behaviour the threat model
    already declares trusted.

docs/security.html stops calling #6 an open design question and points at the
new section; #6 stays as the record of the discussion.

site.js is the file GitHub Pages actually serves, and no Go test could see it.
The gap showed itself as soon as the file was executed rather than read: the
URL check was a startsWith on the raw string, so

  .../releases/download/v9/../../../evil   ->   https://github.com/DevCop95/evil

survived it. A wrong API field name, a broken URL check or a renamed element id
in index.html would all fail silently too: the reader gets an empty download
table and no error on either side.

docs/site.test.js runs the real file against a minimal DOM stub, no
dependencies and no network:

  A  the real API shape (7 assets) fills every column, including the SHA-256
     from checksums.txt and the em dash for assets it does not cover
  B  a hostile payload (javascript: URL, foreign host, ../ traversal, raw HTML
     asset name) yields exactly one row — the on-path one — and the HTML name
     lands in textContent, not markup
  C/D nothing usable, and a 403, both fall back to the pinned list and both
     name the actual reason in the status line
  E  checksums.txt unreachable: the table still renders, hashless
  F  the hand-written fallback table in index.html keeps matching the published
     checksums.txt and the API byte counts humanised (5.8 MiB / 5.4 MiB)

The element ids come from index.html, so renaming one there fails here instead
of silently emptying the page. A static check also fails if any code path
starts building markup from strings.

Fixtures are trimmed copies of a real release response, so the gate cannot go
red because GitHub was slow. SITE_TEST_LIVE=1 runs the same scenarios against
the live API; CI never sets it. Verified under node 20 (what the job pins) and
node 24.

New required check: "site js test".
…ECURITY.md

Closes #6 as a documentation task rather than a code change, which is what the
issue asked for. Two boundaries are now stated where the next reader will look,
instead of sitting in an issue thread:

1. The token is a lock on the door, not on the room. GET / hands it to the
   page and cannot tell a browser apart from any other local process with
   loopback access. Spell out the two deployment cases: on the single-user
   workstation the token adds no exposure beyond what a same-user process
   already has, and on a shared or RDP host an unprivileged process that reads
   the token can drive an elevated --enable-collect server. Name the fix (named
   pipe with a SID ACL) and the issue that holds the rejected options.

2. POST /v1/sys/opt writes HKLM policy and disables DiagTrack with no flag and
   no body. Documented here rather than gated, because gating it would change
   behaviour the threat model already declares trusted.

docs/security.html stops calling #6 an open design question and points at the
SECURITY.md section; #6 remains as the record of the discussion.
@DevCop95
DevCop95 merged commit 1ea400f into main Oct 18, 2026
6 checks passed
@DevCop95
DevCop95 deleted the test/site-js-and-boundary branch October 18, 2026 04:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant