fix(ops): refuse a synthetic-journey target that is not ours

S1. The journey signs in with a real account and POSTs that account's
password to whatever BASE_URL names, and the workflow took BASE_URL as
free text. Anyone with repo write, or a leaked token, could dispatch the
run at a collector of their own and keep a working credential; secret
masking guards the log, never the egress.

The host now has to be on a short const allowlist in the script, checked
before anything is sent, with exit 2 and a named refusal otherwise. The
workflow input is a choice between the two hosted instances, so the form
cannot offer a third; the script's own check still stands behind it for a
dispatch that goes round the form.

--self-test covers the refusal, the prefix lookalike a startsWith check
would have waved through included.
This commit is contained in:
Marc-André Bombeck committed 2026-09-10 01:25:01 +02:00
1 parent 8995bef61c
commit 742eeaad3c
2 files changed
+91 -6

No files matched your search

+13 -3
View File
@@ -21,10 +21,19 @@ name: Synthetic journey
on:
workflow_dispatch:
inputs:
# A closed list, not free text. The dispatcher chooses where the probe
# account's password is POSTed, so the choice is between the hosted
# instances and nothing else. The script re-checks the host against its
# own const allowlist and exits 2 on anything else, so a dispatch that
# goes round this form still cannot reach a stranger's collector.
base_url:
description: Instance to walk, e.g. https://review.healthlog.dev
description: Instance to walk
required: true
type: string
type: choice
default: https://review.healthlog.dev
options:
- https://review.healthlog.dev
- https://healthlog.bombeck.io
expected_version:
description: Version the instance must serve, e.g. v1.38.13
required: true
@@ -53,7 +62,8 @@ jobs:
run: node scripts/synthetic-journey.mjs --self-test
# Every input and secret rides an env var — never interpolated into
# the shell command.
# the shell command. The script refuses a `BASE_URL` whose host is off
# its allowlist before it sends the password anywhere.
- name: Walk the journey
env:
BASE_URL: ${{ inputs.base_url }}