Skip to content

Commit e3d2fb3

Browse files
Merge pull request #71 from StepenkoAnatoli/fix/logs-echo-dispatch-inputs
The job log echoes every dispatch input, and a signed-in reader sees it
2 parents 72efd95 + 22f7def commit e3d2fb3

4 files changed

Lines changed: 146 additions & 7 deletions

File tree

‎.github/workflows/collect.yml‎

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,36 @@ name: collect
5454
# a run - that one happened, when, how long it took, what the steps were called, that an
5555
# artifact of N bytes exists - and not its subject or its contents.
5656
#
57+
# AND THE SIGNED-IN SURFACE IS DIFFERENT. MEASURED 2026-09-22, AND IT IS NOT SAFE.
58+
#
59+
# The paragraph above is scoped to ANONYMOUS REST, where it still holds. This comment used
60+
# to leave the rest at "treat that as unknown rather than safe". It is no longer unknown.
61+
#
62+
# Runs 35762363585 and 35762574315, checked both ways on the same run:
63+
#
64+
# anonymous, web UI run page the input values appear NOWHERE in 236 KB of HTML,
65+
# not even collapsed. Run name, status, duration, step
66+
# names, annotations, artifact name/size/digest only.
67+
# anonymous, web UI job log "Sign in to view logs" - refused, agreeing with REST 403.
68+
# SIGNED IN, job log EVERY dispatch input passed through `env:` is echoed
69+
# VERBATIM, because Actions prints a step's env block:
70+
# TOPIC: probe
71+
# PRIOR: PROBE-START length=1200 xxxxxxxx...
72+
#
73+
# So on a PUBLIC repository the topic and the prior are readable by anyone with a GitHub
74+
# account. Not by a passer-by, and not a secret either. The 403 is an authentication wall,
75+
# not a need-to-know one.
76+
#
77+
# Nothing here is the place to fix that - passing an input through `env:` is exactly the
78+
# rule that keeps it out of a shell (see below), and routing it around the log would mean
79+
# routing it around that protection. What changes is the CLAIM: do not read "the topic
80+
# appeared in none of the readable responses" as "the topic is private". Research a subject
81+
# you would not publish, and use a private repository or a different route.
82+
#
83+
# bin/disclosure.mjs cannot see this and should not: it probes unauthenticated on purpose,
84+
# because a probe that quietly authenticated would answer a different question. A clean
85+
# disclosure report means "a stranger learns the shape", never "nobody learns the subject".
86+
#
5787
# That is why the naming rules above are the primary control rather than a minor one: the
5888
# names are the disclosure surface, and they are the part this workflow governs.
5989
#
@@ -90,10 +120,24 @@ on:
90120
# it - its own "10"s are about concurrency and background steps. So treat three as the
91121
# remaining budget, not as a guarantee, and expect a loud failure rather than a quiet one.
92122
#
93-
# The other half of that check came back UNANSWERED: nothing found says whether a single
94-
# input value has a length limit. It matters here because `prior` is free text, and
95-
# silent truncation would corrupt a prediction while leaving it looking intact. Unknown,
96-
# and recorded as unknown.
123+
# PER-INPUT LENGTH: NOTHING TRUNCATES. MEASURED 2026-09-22, after the corpus failed.
124+
#
125+
# This was recorded as unknown, and search could not close it. The one on-topic capture
126+
# was a 2024 community question asserting "its length is limited to 1024 characters",
127+
# whose only reply is a dormancy bot - nobody ever confirmed it. So it was measured
128+
# instead, by dispatching a payload of known length with a marker at the very end:
129+
#
130+
# 1200 chars run 35762363585 accepted, arrived as 1200, PROBE-END intact
131+
# 8000 chars run 35762574315 accepted, arrived as 8000, PROBE-END intact
132+
#
133+
# The 1024 figure is NOT what this repository observes, and the dangerous case does not
134+
# occur: no silent truncation, so a registered prior cannot be quietly corrupted while
135+
# still looking intact.
136+
#
137+
# What is still unknown, stated rather than rounded off: WHERE the ceiling is. It is
138+
# above 8000, and locating it costs one dispatch per probe, so it was not bisected. A
139+
# realistic prior is a few hundred characters, which is an order of magnitude inside
140+
# what has actually been shown to work.
97141
inputs:
98142
topic:
99143
description: 'What to research. Visible to anyone who can read this repository.'

‎docs/adr/0035-a-privacy-claim-is-measured-not-asserted.md‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,3 +186,57 @@ cannot make — the same reason `client_ref` warns rather than refuses.
186186
- `bin/disclosure.mjs` is read-only and unauthenticated, so it is safe to run against a
187187
repository you do not own — which is also the only way to check somebody else's claim
188188
about their own exposure.
189+
190+
## Amendment, 2026-09-22 — the signed-in surface was measured, and it is not safe
191+
192+
This ADR left a hole and named it: *"what a signed-in user with read access sees in the web
193+
UI, which is a different surface from the REST API. Treat that as unknown rather than safe."*
194+
195+
It is no longer unknown. Runs `35762363585` and `35762574315`, checked both ways:
196+
197+
| surface | result |
198+
|---|---|
199+
| anonymous, web UI **run page** | the dispatch input values appear **nowhere** in 236 KB of HTML, not even collapsed |
200+
| anonymous, web UI **job log** | *"Sign in to view logs"* — refused, agreeing with the REST `403` |
201+
| **signed in**, job log | **every input passed through `env:` is echoed verbatim** |
202+
203+
The third row is the finding. GitHub Actions prints a step's `env:` block into the log, so
204+
the log carries:
205+
206+
```
207+
TOPIC: probe
208+
PRIOR: PROBE-START length=1200 xxxxxxxx...
209+
```
210+
211+
**On a public repository, that makes the topic readable by anyone with a GitHub account.**
212+
Not by a passer-by — the `403` is real — but the wall is authentication, not need-to-know.
213+
214+
### What this changes, and what it does not
215+
216+
**The measurement in the body above stands.** Every row of it was scoped to unauthenticated
217+
REST and every row is still true. What was wrong is the inference a reader would draw from
218+
it, and this ADR is the reason they would draw it: a table of refusals invites the
219+
conclusion that the subject is private, and it is not.
220+
221+
**It is not fixed here, deliberately.** Passing an input through `env:` is the same rule
222+
that keeps it out of a shell (ADR-0020). Routing it around the log would mean routing it
223+
around that protection, and trading a disclosure to signed-in readers for a code-execution
224+
surface is a bad trade. The honest response is the accurate claim, not a workaround.
225+
226+
**`bin/disclosure.mjs` is unchanged and should be.** It probes unauthenticated on purpose —
227+
a probe that quietly authenticated would answer a different question while looking like this
228+
one. What changes is how its output must be read: a clean report means *a stranger learns
229+
the shape*, never *nobody learns the subject*.
230+
231+
### The rule this is the third instance of
232+
233+
Twice this ADR has recorded something that felt settled and turned out untrue when run —
234+
first the header's claim that inputs are visible, then this. Both times the error was in the
235+
step from *what was measured* to *what it means*, not in the measurement.
236+
237+
So the standing correction is narrower than "measure it": **state the scope of the
238+
measurement in the same breath as its result**, because a true finding with its scope left
239+
off is how an overstatement gets built on top of honest work.
240+
241+
**Practical consequence:** research a subject you would not publish, and this route is the
242+
wrong one. Use a private repository, or collect locally.

‎research-kit/README.md‎

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -273,8 +273,24 @@ That is why keeping the topic out of the run name and the artifact name is the *
273273
control rather than a minor one: those names are the disclosure surface, and they are the
274274
part the workflow governs.
275275

276-
**Not measured, and so not claimed:** what a signed-in user sees in the web UI, which is a
277-
different surface from the REST API. Treat it as unknown rather than as safe.
276+
**Measured 2026-09-22, and it is not safe: a signed-in reader sees the topic.** This used to
277+
say the web UI was unknown. It is not. Actions prints a step's `env:` block into the job
278+
log, and every dispatch input reaches a step through `env:` — so the log carries
279+
`TOPIC: <your topic>` verbatim. Anonymously the log is refused (`403`, and the web UI says
280+
*"Sign in to view logs"*), but on a **public repository that wall is authentication, not
281+
need-to-know**: anyone with a GitHub account is through it.
282+
283+
The anonymous table above is still true, every row. What was wrong is the inference — a list
284+
of refusals reads like "the subject is private", and it is not.
285+
286+
This is not worked around, deliberately: passing inputs through `env:` is the same rule that
287+
keeps them out of a shell, and routing around the log would mean routing around that. And
288+
`disclosure.mjs` cannot see it *by design* — it probes unauthenticated, because a probe that
289+
quietly authenticated would answer a different question. Read a clean report as **"a
290+
stranger learns the shape"**, never as "nobody learns the subject".
291+
292+
**So: to research something you would not publish, do not use this route.** Use a private
293+
repository, or collect locally.
278294

279295
**Going private is not the reflex fix.** On a GitHub Free plan — which this account is on —
280296
converting a repository to private makes its protection rules and environment secrets
@@ -397,7 +413,7 @@ node research-kit/bin/selftest.mjs # all of it
397413
node research-kit/bin/selftest.mjs gate hook # just these files
398414
```
399415

400-
888 tests, offline, no key and no network. The runner **awaits** every test, so `ok` means
416+
889 tests, offline, no key and no network. The runner **awaits** every test, so `ok` means
401417
the assertions settled (ADR-0021), and each test is raced against a watchdog
402418
(`RESEARCH_KIT_TEST_TIMEOUT`, default 60s).
403419

‎research-kit/test/collector-workflow.test.mjs‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,31 @@ test('the caller reference is declared PUBLIC where a dispatcher will read it',
206206
'the run should also warn on the run itself, where the reference was actually used');
207207
});
208208

209+
test('the anonymous disclosure table never appears without the signed-in caveat', () => {
210+
// The rule ADR-0035's amendment exists to enforce: state the SCOPE of a measurement in
211+
// the same breath as its result.
212+
//
213+
// The anonymous table is true and was honestly obtained - 403 on logs, 401 on artifacts,
214+
// the topic in none of the readable responses. Printed alone it still misleads, because a
215+
// list of refusals reads as "the subject is private" and it is not: Actions echoes a
216+
// step's `env:` block into the job log, every dispatch input arrives through `env:`, and
217+
// on a public repository anyone with an account can read that log.
218+
//
219+
// Neither half is deleted. A correction that erases what it corrected teaches nobody, and
220+
// this pins that the two travel together wherever the table is repeated.
221+
const sources = [
222+
['collect.yml', yaml ?? ''],
223+
['research-kit/README.md', fs.readFileSync(path.join(KIT_ROOT, 'README.md'), 'utf8')],
224+
];
225+
for (const [name, text] of sources) {
226+
if (!/job \*?\*?logs?\*?\*?[^\n]*403/i.test(text)) continue;
227+
assert.ok(/signed[- ]in/i.test(text),
228+
`${name} prints the anonymous refusal table without saying what a signed-in reader sees`);
229+
assert.ok(/env:/.test(text) && /\blog\b/i.test(text),
230+
`${name} does not name the mechanism - the job log echoing a step's env: block`);
231+
}
232+
});
233+
209234
// ---------------------------------------------------------------- correlation
210235

211236
test('the dispatch API version is pinned, and the documented one', () => {

0 commit comments

Comments
 (0)