plugin add grants derived_names by default and never names it on the screen the operator answers #277
Labels
No labels
code-review
correctness
dos
performance
security
severity/high
severity/low
severity/medium
tech-debt
Kind/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
h-dv/code-index#277
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found in an adversarial security review during #268. Pre-existing, not introduced by that work. It is the one finding of that review I would act on first, because the threat model rests an argument on a lever that is invisible at the moment it is pulled.
What the threat model claims
_prdoc/guides/80-threat-model.mdcallsderived_namesthe whole of §4's weakness, and rests the defence on the operator's lever existing since #103.verify_grant's rule 5 states what the authority is:Measured
plugin addwith no--grantcrates/cli/src/plugin.rs:1914approval::GrantSpec::RequestedRequestedresolves the flagcrates/indexer/src/approval.rs(requested_derived_names)crates/cli/src/plugin.rs:1974-1990.with_derived_names(...)crates/indexer/src/confirm.rsderived_namesin the entire fileThe
Disclosurestruct carriesgrant,withheld,bridge_grant,bridge_withheld,domain,language_profiles,displaces,activation_owner,writer_lock,notes. There is no field for it, andVerifiedGrant::render()returns capabilities and bridges only.The path
A signed package requests
[capabilities] derived_names = true. The operator runsplugin add, reads a block listing two or three capability names plus theMEANSsentence about signatures, and presses the key labelled "approve as requested". They have granted the authority above, and nothing on screen said the word.--yesrequires--grant(plugin.rs:1652), but--grant requestedcarries it silently too.Why this is worse than an ordinary disclosure gap
The value is disclosed everywhere except the decision point:
plugin statusprintsderived_names=yes|no(crates/cli/src/plugin.rs:3982-3995)plugin enable --derived-namesrequires the flag explicitlyplugin inspectshows the requestSo the information exists, is formatted, and is shown — after the answer has been given. An operator who audits later can see what they granted; an operator answering the prompt cannot see what they are granting.
It also means the threat model's §4 argument is, as written, a claim about a lever rather than about the screen. The lever is real. The screen is where it is pulled.
Scope
No shipped first-party package is affected in the sense of exploiting this —
de.h-dv.rubyrequestsderived_names = truefor a real and documented reason (Rails'has_many :postsemits atyperef namedPostat the span of the literal:posts), and that request is legitimate. The finding is that a malicious package's identical request is answered by the same keystroke, with the same screen.Fix
Add
derived_namesto theDisclosurestruct and render it inconfirm.rsbesidegrant/withheld. It is a one-field change to the screen the whole of §4 rests on.Worth considering alongside it, though each is a separate decision:
GrantSpec::Requestedgrant it at all? Every other high-authority answer in this tree is opt-in.Requestedmeaning "everything the manifest asked for, including this" is defensible only if the screen names it.VerifiedGrant::render()is capability-name-shaped andderived_namesis a bool, which is presumably why it never got a row. That is a reason for the omission, not a justification.Related, from the same review — reported here because the fix may as well cover both
crates/abi/src/frame.rs:53-63argues that a package emittingTAG_SYMBOL_BASENAMEdeclaresfact_minor_min = 2so an older host refuses it rather than storing the guest's placeholder as a name. Nothing enforces the first clause.Manifest::validaterefusesfact_minor_min > ABI_MINOR, but no site couples EMISSION of tag0x8004to the DECLARATION. A third-party package declaringfact_minor_min = 0and emitting the tag installs on anABI_MINOR = 1host, whose decoder skips the tag and stores the placeholder verbatim — the exact MISLEAD case the bracket is documented as preventing.de.h-dv.sveltedeclares it correctly; this is a trap for a well-meaning third-party author, and the threat model states it as a system property when it is a convention. The enforceable point isplugin check's conformance leg, which holds both theValidatedFacts(hencename_is_basename) and theManifest.TAG_SYMBOL_BASENAMEto thefact_minor_mindeclaration that is supposed to bracket it #280Fixed in
d5547cc— 11/11 CI greenderived_namesis now on the screen the operator answers.It sits with the capability lines because that is what it is: an operator scanning "what am I granting" reads those four, and a fifth authority rendered anywhere else is one this surface disclosed without putting it where the answer is formed.
Four states, not a bool
not requested·WITHHELD (requested by this package)·GRANTED·not decided here — <why>.A bool collapses the first two, and they are different facts about different packages — one never asked, one asked and was REFUSED.
capability_withheldalready draws that distinction for the pool capabilities; this is the same distinction for the authority that outranks them.The fourth state was not in the plan. There are NINE
Disclosureconstruction sites, not one, and they are not interchangeable: onlyadd,enableandcheckdecide this authority (checkbecause it RUNS C1 under it and takesderived_namesas a parameter). The other six grant nothing, and renderingnot requestedonplugin gcwould state a fact about the package on a screen that is not about the package's grant — a default that reads like an answer, which is the same defect as the silence being fixed. They rendernot decided hereplus which command does decide, mirroringDomain::Absent's existing "no value here, and here's why".Graded
the_confirmation_names_the_derived_names_answeranda_refusal_and_an_absent_request_do_not_render_alike, with three mutations run:derived_namespush fromdisclosure_text5 passed; 2 failed)NotRequestedandWithheldinto one armassert_ne!firesA first reading of the first mutation reported ONE failure. It was wrong: the log was piped through
tail -30before the grep counted it, so the count described what survived the truncation rather than the run. That correction is recorded on the test, because it is the mistake a reader would repeat.NOT fixed here — split to #280
Two items from this issue are not addressed by
d5547cc, and closing without saying so would lose them:0x8004to thefact_minor_mindeclaration meant to bracket it. Different defect, different fix location (plugin check's conformance leg, the only place holding both theValidatedFactsand theManifest). Now #280.GrantSpec::Requestedshould grant this authority at all by default. Raised here as a question rather than a defect, and it stays one — the reported hole was the screen, and changing what--grant requestedMEANS is a behaviour change for anyone scriptingplugin add. Carried into #280 for a decision.Verified: fmt 0, clippy
-D warnings0 (workspace, all targets), rustdoc-D warnings0,cargo test --workspace359 binaries / 3927 passed, and all 11 CI jobs green ond5547cc.One local suite failure was attributed rather than waved through:
an_idle_worker_holding_a_compiled_grammar_costs_what_this_saysreported a worker WITH a compiled grammar costing 6231 KiB against 13398 KiB WITHOUT — inverted, so the "difference of 0 KiB" was a saturating subtraction on contended RSS readings while six review agents were running. Isolated 4/4 green, and CI'scargo teston a quiet runner confirms it.