Repository navigation
Authorization findings don't fit the contract-dimension matrix — proposal for a claimed-boundary dimension #8
Description
I read through principles.md and the §1.1–§1.19 output spec after Michael's note in the OpenSSF Slack. The framing — a contract that lets a triager route a report to in-model / out-of-model / by-design — is the artifact most projects are missing when a report lands. This is a comment on one class the current structure doesn't route cleanly.
The gap
The contract-dimension matrix asks whether the project claims, disclaims or hasn't decided edge semantics: overflow, callback failure, cycles, serialization, lifecycle, resource bounds. Those are all behavioural dimensions — properties of what the code does with input it accepts.
Authorization isn't shaped like that. The question isn't "what does this function do with a bad value," it's which caller was this data supposed to be reachable by, and does the check run per-record or per-class?
Concretely, the reports that are hardest to triage look like:
• a parent object is correctly scoped to the caller, but its child records are fetched without re-applying the scope
• a permission check runs against the collection or the class, not the specific record being returned
• an identifier supplied in the request body reaches a data path that no guard evaluates, because the guard is on the URL parameter
• one interface enforces a role and a sibling interface over the same operations doesn't
None of these look like a trust-boundary crossing in the input-trust sense. The input is well-formed and the caller is authenticated. What is violated is a boundary the project never wrote down — which is exactly why a triager stalls on them, and exactly what a contract could fix.
Suggested addition
A dimension recording, per resource or resource family, which boundary the project claims:
• scope claimed — per-record / per-tenant / per-collection / none
• enforcement point — where the check runs (route, controller, query layer, not enforced)
• disclaimed — e.g. "this endpoint is intended to be reachable by any authenticated user"
That last one is doing most of the work. A documented disclaimer — "any authenticated caller may read any record in this table" — closes an entire class of inbound reports as by-design, verifiably, without the maintainer re-deriving the reasoning each time. It fits the principles.md preference for a documented disclaimer of the safe direction over an open question.
A false friend worth naming in the recurring-false-positives section
Sibling asymmetry is not, by itself, a finding. Two endpoints handle the same object and one applies a narrower check than the other. It is tempting — for a human, and much more so for an agent doing variant analysis — to conclude the weaker one is missing a control. But an inconsistency between siblings is just an inconsistency. The weaker path can be the intended one, and the stronger one can be defence-in-depth or an artefact of unrelated history.
I've had a finding withdrawn on exactly this basis, where the maintainer's determination was that the broader path was intended. The asymmetry was real; the vulnerability wasn't.
This matters for the tool specifically: a model that records which boundary is claimed retires that false positive automatically, whereas a model recording only observed behaviour will keep re-raising it. That is a concrete argument for the dimension above rather than a nice-to-have.
Offer
Rather than more opinions, I'd rather contribute ground truth. I have a set of disclosed authorization findings where I know the maintainer's actual ruling — accepted and fixed, or determined to be by-design — across a range of projects. I'm happy to run the generator against several of those projects, feed the corresponding reports through threat-model-triage, and write up where the disposition it produces agrees or disagrees with the ruling the maintainer actually gave.
That would exercise the backtest path against real outcomes rather than synthetic ones, and it would say something useful about the tool either way. Happy to do it as a PR against tests/ if there's a shape you'd prefer the results in.
Activity
Santoshkumarpuppala commentedon Aug 8, 2026
Following up on the Slack thread — taking the placement point first.
Fair enough on the contract not being the right home for the per-record/per-class distinction. If the model's job is categorizing inbound reports rather than describing enforcement, a scanning-side signal is the better fit, and I'll leave that.
The rest of this is about ground truth, since that's the part you said was hardest.
What I can offer
Labeled outcomes for authorization findings, on both sides of the line.
Positives — reported, confirmed by the maintainer, fixed. Each carries the affected version, the endpoint and method, the root-cause pattern, and the fixing commit or patched release. The fix reference matters: it gives a vulnerable/patched pair rather than a bare assertion.
Negatives — and this is the half I think is harder to source. Candidates that looked like findings, were investigated, and were rejected, each labeled with the reason:
• by-design — a role already entitled to the capability, or a library trusting its caller
• guard one layer up — the check exists, just not where you expected it
• not release-present — the flaw is only on main, not in anything shipped
• writable but not honored — a settable field that no security decision actually reads
• single-tenant — no boundary to cross in the deployment model
• sanitized front door — the sink is real but unreachable through any exposed path
A system that only sees confirmed vulnerabilities has no basis for declining anything. The rejects are where the restraint gets learned, and they're rarely written down because nobody publishes the things they decided weren't bugs.
Constraints, stated up front
Anything still under coordinated-disclosure embargo stays out. A first slice drawn only from cases tied to published advisories avoids that entirely and is independently checkable — every entry can point at a public advisory or CVE record.
For rejected candidates, where the assessment came out of a private exchange with a maintainer I'd describe the pattern rather than name the project. The label is what carries the value; the identity mostly doesn't.
Proposal
I'll assemble a first slice from published cases as JSONL — roughly
{project, version, endpoint, class, verdict, reason, reference}— and open it as a PR againsttests/, unless the harness expects a different shape. If a different field set is easier to ingest, say so and I'll match it.If it's useful, I can also run the generator against a few of those projects and report where its disposition agrees or disagrees with what the maintainer actually decided. That would test the backtest path against real outcomes rather than synthetic ones.
Santoshkumarpuppala commentedon Aug 13, 2026
Following up on the offer at the end of this issue, with something concrete rather than another question about format.
Two things I can hand over. The first is ready now.
1. Valid cases — public advisories only
Authorization findings where the maintainer's ruling is on the public record, with the fixing release recorded. The fixed version is the field I'd argue matters most, since it gives a vulnerable/patched pair rather than a bare assertion — you can check the tool's disposition against both sides.
A sample of the shape, five rows across different languages and sub-patterns:
{"project":"invoiceshelf","language":"php/laravel","advisory":"GHSA-vgx6-6cqr-m8qr","cve":"CVE-2026-55610","cwe":"CWE-639","pattern":"cross-tenant-idor","endpoint":"GET|PUT /api/v1/users/{user}","fixed":"2.4.1","verdict":"valid"}
{"project":"mailwatch","language":"php","advisory":"GHSA-w7v5-7g6w-87r4","cve":null,"cwe":"CWE-639","pattern":"read-scoped-write-unscoped","endpoint":"GET /quarantine_action.php","fixed":"1.2.27","verdict":"valid"}
{"project":"openfoodnetwork","language":"ruby/rails","advisory":"GHSA-7cqp-qvh5-7x85","cve":null,"cwe":"CWE-862","pattern":"scope-mass-assignment","endpoint":"POST /api/v1/customer_account_transaction","fixed":"5.7.4","verdict":"valid"}
{"project":"nautobot","language":"python/graphene-django","advisory":"GHSA-mfwj-pjgx-22v2","cve":null,"cwe":"CWE-285","pattern":"nested-resolver-skips-object-permission","endpoint":"POST /graphql","fixed":"2.4.38,3.2.0","verdict":"valid"}
{"project":"kestra","language":"java/micronaut","advisory":"GHSA-hpj9-grjp-7vc7","cve":null,"cwe":"CWE-306","pattern":"auth-filter-scope-gap","endpoint":"GET /env (management port)","fixed":"2.0.0-rc6","verdict":"valid"}
Every row points at a published advisory, so each one is checkable without taking my word for it. A couple of caveats I'd rather state than have you find: two of the public cases are co-credited with another reporter, and one has a merged fix with no numbered release yet, so its fixed field is honestly empty rather than guessed.
2. Invalid cases — the half that's harder to source
This is the part I think is worth more to you. I keep a stub for every candidate that looked like a finding, was investigated, and was rejected, labeled with the reason it died. That's currently on the order of 550 entries.
The distribution is itself a result, and I don't think it's what most people would guess. Approximately:
• by-design / the caller was already entitled — about half of all rejects
• writable but not honored — a settable field no security decision reads
• not release-present — the flaw exists only on main, not in anything shipped
• guard one layer up — the check exists, just not where you looked
• dead code or unreachable dispatch — a real sink the router never instantiates
• sanitized front door — the sink is real but every external caller sanitizes
• single-tenant — no boundary to cross in the deployment model
• unguessable capability token — no owner check, but the key can't be enumerated
If half of all plausible authorization candidates fail because the caller was already entitled to the capability, then a triager's hardest call is almost never "is this reachable" — it's "was this supposed to be reachable." Which is the same claimed-boundary question this issue opened with, arriving from the data side rather than the design side.
Scope
The valid set is drawn only from published advisories, so nothing under embargo is involved. For the rejects, I'd start at the pattern level with anonymised examples — the label is what carries the value and the project identity mostly doesn't. Where a named case would genuinely help you test against a specific project, I can release those individually after checking each one doesn't expose anything unfixed.
One question
Where would you want this to live, and in what shape? I can open it as a PR against tests/ in whatever format the harness reads, or as a standalone file if that's cleaner. JSONL above is a guess, not a preference.
Happy to run the generator against several of these projects afterwards and report where its disposition agrees or disagrees with the maintainer's actual ruling, which was the original offer and still stands.
scovetta commentedon Aug 19, 2026
@Santoshkumarpuppala I added some auth'z analysis via #13, want to give it a whirl?
Santoshkumarpuppala commentedon Aug 28, 2026
Gave it a whirl. Thanks for building the dimension out — seeing it land as the ninth contract dimension with its own glossary entries, question-bank items and triage routes is a much fuller treatment than what I proposed here, and the routing decision (authorization findings take existing routes, no new disposition) is the right call.
Ran the deterministic tiers against #13 at
beab30e: 304 passed, and the golden zlib model validates clean at 43 checks. Two things came out of it, both in the harness rather than the guidance.1. Three of the four enum values #13 adds have no test coverage.
Method: remove one value from its enum, run the full suite, restore.
"authorization"(_PROPERTY_KIND)"authorization-scope"(_DIMENSIONS)"credential"(_CONTROL_KIND)"principal"(_CONTROL_KIND)authorization-scopeis the control — a covered value does break the suite, so the three zeros mean the coverage is absent rather than that the experiment is broken. The Copilot review flagged"authorization"; the same holds for"credential"and"principal". A later edit dropping any of the three would silently reject every sidecar using them with CI still green.2.
G2.contract-dimension-matrixpasses on a model that has no authorization-scope row at all.checks.pyjoins the whole §1.7 table into onematrix_textand teststok in matrix_textper dimension. For this dimension the token is"authorization", and a substring search over the whole table cannot distinguish a dimension row from the word appearing in another row's conditions, route or provenance.On the golden zlib model: deleted all three
| authorization scope |rows, appended "(authorization enforced by caller)" to an unrelated row's conditions cell, corrected the header claim tally 71 → 68 so the missing dimension was the only remaining defect. Result:Control: remove that one injected phrase and nothing else →
So the check can fail; it just cannot tell the difference between the dimension being present and the word being present.
To be fair to the design: the substring mechanism is pre-existing and applies to all nine tokens — #13 only added the ninth. But it bites hardest here, because "authorization" is a word that plausibly turns up in other rows' prose, where "reentrancy" and "reconstruction" do not.
Suggested fixes: match on the row's dimension cell rather than on
matrix_text; and add one sidecar fixture per new enum value so the Tier-1 mutation tier covers them the way it already coversauthorization-scope. Happy to send that as a PR — say the word and I will, or leave it to you if you would rather keep the harness in one pair of hands.Separately, and not for this thread: the triage guidance in #13 says sibling asymmetry is not by itself a finding, and routes on the claimed scope with
MODEL-GAPwhere no claim exists. I think that is right in principle and I would like to come back to it with data rather than an opinion — in my own corpus of coordinately-disclosed authorization findings, sibling paths where one is correctly scoped and the other is not are the single most common shape, and for a project with no recorded claim the rule sends them all toMODEL-GAP. Worth testing against real routing outcomes before it hardens. I would rather open that as its own issue with numbers attached than bolt it onto this one.Not checked: Tier 3 (needs a judge model), Tier 5 (needs the networked
fetch_replaybuild step), and live end-to-end generation.Santoshkumarpuppala commentedon Sep 3, 2026
Sent it: #14.
Both defects are on
mainand neither depends on #13 — the substring matcher already could not tellresource complexitythe dimension from "complexity" the word, and the enum coverage gap covers nine pre-existing values, not just the three #13 adds.The coverage number came out larger than I reported here. Running the remove-one-value-and-rerun sweep across all three enums on
main: 9 of 25 uncovered —authentication,confidentiality,output-sanitizationin_PROPERTY_KIND, andcallback-code,collaborator-implementation,object-topology,rate,serialized-state,type-classin_CONTROL_KIND. All eight_DIMENSIONSvalues are caught, which is the control that makes those nine zeros absence rather than a broken sweep.One consequence worth flagging for #13 specifically: with the inventory test in place, #13 will fail it until
authorization,credentialandprincipalare added to the expected sets — and the round-trips then prove the validator actually accepts them rather than just listing them. That is the behaviour I would want, but it does mean #13 needs a two-line update after #14 lands.303 → 322 passed, and the same sweep after the fix catches 25 of 25.