Skip to content

fix: scrub denied bug ids from bodies, history and comments - #19

Merged
plusky merged 1 commit into
mainfrom
fix/scrub-denied-bug-ids
Jul 28, 2026
Merged

fix: scrub denied bug ids from bodies, history and comments#19
plusky merged 1 commit into
mainfrom
fix/scrub-denied-bug-ids

Conversation

@plusky

@plusky plusky commented Jul 27, 2026

Copy link
Copy Markdown
Owner

The hole

A bug the policy hides could still name itself inside a bug the client was
allowed to read:

  • bug_info fetched with include_fields: None, i.e. Bugzilla's default
    field set — so blocks, depends_on, dupe_of and see_also carried the
    ids of hidden bugs straight to the client.
  • bug_history returned change records untouched, and those record
    precisely which bug was linked or unlinked, by whom, and when.
  • bug_comments passed through Bugzilla's own
    *** Bug N has been marked as a duplicate of this bug ***.

Uniform denials (I2) are pointless when the neighbouring bug names the secret.
The asymmetry was stark: update_bug_dependencies and mark_as_duplicate
already assess every link target before writing one, on the grounds that
such a link leaks existence — while every read path handed the same links back
out for free.

The rule

An id may be named inside a served body only if it earns
Capability::Summary — the same bar the write paths use (I8/I11), recorded
now as I14.

Candidate ids come from Bugzilla's answer, not from the client, so unlike
assess they are judged in one batched request whatever their number: a
client cannot choose them in order to probe with them. A failed assessment
scrubs everything (I4).

see_also is matched against this instance's own base URL — a link to another
tracker is not ours to scrub, and a host that merely starts with ours is not
ours to trust either.

What it does to the output

surface before after
blocks/depends_on/regressed_by/… hidden ids listed hidden ids removed
dupe_of hidden id null
see_also local hidden bug URL dropped; foreign trackers kept
history change naming only hidden bugs served dropped, not served empty — an empty change still marks that something happened
duplicate-marker comment served dropped

Limits, deliberate and recorded

Free text where a person writes a bug number is untouched: recognising that
would mean gutting comments. And the duplicate marker is matched as template
text
, so an instance with localised or customised templates evades it — a
mitigation for what Bugzilla itself writes, not a guarantee. Both are in
DESIGN.md under I14 rather than left for a reviewer to discover.

Verification

157 tests (+6). They pin: ids collected from every id-bearing field including
see_also URLs in both forms; only non-disclosable ids removed while foreign
trackers survive; dupe_of blanked; history changes edited down and
fully-scrubbed entries dropped; duplicate markers recognised but ordinary
prose mentioning a bug number not mistaken for one; and see_also matching
anchored so evil.example and other.cgi?id= are not treated as local links.

A bug the policy hides could still name itself inside a bug the client was
allowed to read. bug_info returned Bugzilla's default field set, so
blocks, depends_on, dupe_of, see_also and url carried the ids of hidden
bugs; bugs_quicksearch let the client CHOOSE those fields and returned
them wholesale; bug_history returned change records untouched, and those
record exactly which bug was linked or unlinked and when; bug_comments and
summarize_bug passed through Bugzilla's own duplicate markers. Uniform
denials are pointless when the neighbouring bug names the secret.

The write paths already refuse to CREATE such a link without assessing the
target — reading one back out must not be the way around that. So the same
bar applies: an id may be named inside served content only if it earns
Capability::Summary, recorded as invariant I14 and applied on all five
paths.

Only ids actually SERVED in the same response are exempt. A requested id
that was DENIED is not: whitelisting it would mean that naming a hidden
bug in the request reveals it through the links of one the client may
read, turning the scrub into an existence oracle for any id worth guessing.

Candidate ids come from Bugzilla's answer rather than from the client, so
unlike assess() they are judged in one bounded batch whatever their
number. That request is issued even when there is nothing to assess:
skipping it would let the clock separate "this bug has no links" from
"every link is hidden".

Both stock duplicate-marker templates are matched, in both directions —
the one written on the duplicate is preceded by whatever the closer typed,
so neither can be found by looking at the start of a comment. see_also and
url are matched host-and-path, case-insensitively and either scheme, since
Bugzilla stores them as the user typed them.

Limits recorded rather than papered over: free text where a person writes
a bug number is untouched, localised marker templates are not recognised,
and an instance reachable under a second hostname is not matched in
see_also.

Also corrects the previous commit's rationale for dropping the batch in
assess(): whether a batch omits an unserviceable id or fails outright is
deployment-dependent (stock /rest/bug?id=.. omits), so what the old code
actually leaked was the retry that fired only on failure.
@plusky
plusky force-pushed the fix/scrub-denied-bug-ids branch from 8fc148a to 0bbcd16 Compare July 27, 2026 20:32
@plusky
plusky merged commit 30bcb09 into main Jul 28, 2026
10 checks passed
@plusky
plusky deleted the fix/scrub-denied-bug-ids branch July 28, 2026 05:57
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