When a report says “IDOR,” first identify the object, the caller, the action and the rule the caller would break. Pin the source revision and affected route. Trace the supplied identifier to the object lookup, then look for ownership and permission checks in every required layer. If the path remains plausible, compare an authorized own-object request with a foreign-object request using synthetic accounts in an approved environment. Record a finding at the evidence level you have: a complete source trace can justify a repair ticket, while a controlled observation supports a claim about runtime behavior. Keep unresolved deployment, downstream checks or sharing rules visible in that ticket. A visible ID, a findById call, or a scanner label is a lead, not proof.
An insecure direct object reference, or IDOR, is an object-level authorization failure associated with a caller-controlled reference. OWASP's IDOR testing guide recommends identifying reference points and comparing access with more than one controlled user. The underlying question is the same as API1:2023 Broken Object Level Authorization: does the API authorize this action on this particular object?
The report that starts the review
Illustrative case, not a customer finding: Northstar Metrics has Cedar and Harbor accounts. A static alert at revision N-208 flags GET /api/reports/{reportId}/preview. The handler calls reportStore.get(reportId) and returns a generated preview. Priya is an analyst in Cedar. R-82 belongs to Harbor. The alert calls the route “critical IDOR” because the ID appears in a URL and the handler contains no local ownership check.
That description leaves out three decisive facts: whether an upstream service supplies and enforces Priya's current account, whether reportStore.get is already scoped, and whether the preview is private under Northstar's policy. Rewrite the hypothesis without assuming its conclusion:
An authenticated Cedar analyst who supplies the identifier of a private Harbor report may receive its preview if the preview route and its required downstream services do not compare the report's persisted tenant and parent with the caller's current authority.
This wording names the potentially harmful action and the control that would disprove it. It also avoids claiming that anyone actually viewed Harbor data.
Open the method behind the alarming name
The first useful artifact is the implementation of reportStore.get, not another screenshot of the alert. Continue the invented example with this abbreviated source packet. It is explanatory pseudocode, not executed application code:
# route registration at N-208
preview = require_authenticated(require_preview_grant(handle_preview))
# mandatory policy wrapper
context = current_membership(authenticated_subject, selected_account)
allowed_projects = current_preview_projects(context)
store = ReportStore(context.account_id, allowed_projects)
# store implementation; placeholders represent bound values
SELECT id, tenant_id, project_id, preview_key
FROM reports
WHERE id = :report_id
AND tenant_id = :verified_account
AND project_id IN (:authorized_project_ids)
The IN notation describes a bound collection using the database adapter's supported syntax; it is not an instruction to interpolate a list into SQL. An empty authorized set must produce no rows, never remove the restriction.
For Priya in Cedar, the synthetic authorization inputs are account=cedar, projects={C-17}. R-82 belongs to Harbor/H-9 and cannot match this query. If the wrapper is mandatory on the claimed route and the preview file is fetched only for the returned row, the alert's “no local ownership check” argument is defeated in source. The correct disposition is reject this precise source claim, with the route registration, wrapper and store cited together. Runtime behavior remains unvalidated; do not turn that limit into an unresolved source question that has already been answered.
There are three ways this packet could fail to settle the case. The route might bypass the wrapper; the store might fall back to an unscoped lookup on no match; or preview_key might be taken from a second caller-controlled parameter. Each is a concrete follow-up. Merely saying “a downstream service might do something” is no longer a useful investigation plan.
A sharing exception changes the rule, not the evidence standard
Now the product owner supplies a documented exception: Harbor may share a summary preview with a named Cedar member until an expiry. Add a synthetic grant:
share S-9:
report = R-82
recipient = Priya
action = preview.summary
expires_at = 2026-10-20T12:00:00Z
revoked = false
At an earlier authorized time, a successful summary response could be intended behavior. It does not authorize the attachment, editing, another Cedar user, or continued access after expiry. The decision is an explicit union: the normal ownership/project rule or a current matching share grant, followed by the projection allowed by whichever rule authorized the response. Do not simply add OR shared = true to an account query.
| Synthetic request | Applicable policy | Expected output |
|---|---|---|
Priya, R-82, summary before expiry |
Exact S-9 grant |
Summary fields only |
Priya, R-82, attachment before expiry |
No attachment grant | Deny |
| Another Cedar analyst, same report | Recipient does not match | Deny |
| Priya, summary after expiry or revocation | Grant no longer current | Deny new delivery |
This table explains why a report containing only “changed the ID and got 200” is insufficient. It also prevents the team from using a legitimate share feature to dismiss every foreign-object response.
Close one claim without losing the next one
The filled outcome for N-208 is:
Reported path: private preview returned solely because the caller knows a foreign report ID. Source result: rejected; mandatory wrapper supplies current account/project scope and the store enforces both. Sharing: separately permitted only for an exact, current summary grant. Deployment: not validated by this example. Separate lead: the attachment export route uses a different store constructor and needs its own trace.
That final sentence matters. An export defect should receive a new claim identifying its route, inputs and output; it should not keep the disproved preview claim artificially open. Conversely, a preview fix cannot close the export lead.
If runtime validation is needed, use two controlled accounts and synthetic report markers in an approved environment. Establish the own-object positive control, then the denied private report and the permitted/expired share cases. Capture response fields and downstream file access as well as status. A foreign title in an error is still disclosure; a timeout before the handler is still inconclusive. OWASP's IDOR prevention guidance supports scoping object retrieval to permitted resources, while its reporting guidance supplies the wider reporting context.
Decide whether to file, investigate, or reject
| Decision | Minimum evidence | Record next |
|---|---|---|
| File an actionable finding | Independently reviewed actor → route → object lookup → failed required check → unauthorized effect, bound to revision and scope. A controlled observation can corroborate behavior. | Affected action and asset, conditions, impact, fix owner and acceptance evidence. |
| Keep investigating | A required policy, deployed call order, store scope, cache behavior or effect remains unknown. | The missing artifact, owner and review date; provisional response action if risk is urgent. |
| Reject this reported path | An enforced control or intended sharing rule defeats the precise claim for the assessed build and fixture. | Exact counterevidence and revision, plus any other path that still needs separate review. |
If the suspected effect is serious, the owner may contain access while the review continues. That is a risk response, not a declaration that an unverified alert is confirmed. Likewise, a rejected path is not proof that every preview or export route is safe.
Ask for missing evidence without dismissing the reporter
An external reporter may know only the request they sent and the response they received. They may not know your sharing rules, store implementation, or deployed build. Treat those as internal investigation responsibilities. A useful initial response asks for the route, approximate time, the relationship between the controlled accounts, and a redacted description of the unexpected data. Do not require access to additional customers' records to make the report credible.
For Northstar, the key distinction is whether Priya received a summary through S-9 or received a private preview without a matching grant. The team can inspect its own grant records and source to establish that difference. If the original evidence contains another person's information, restrict access to the report and request the smallest additional detail needed. The investigation should not encourage collection of more sensitive content merely to improve a screenshot.
Suppose the reporter used two accounts they control but did not realize both had been added to the same project. The observation can be accurate while the interpretation is wrong. Explain the specific permission relationship that authorized the response. A clear rejection names the relevant policy and evidence without disclosing unrelated account information. “Works as designed” is insufficient when the design was not visible to the reporter.
The reverse situation is equally important. A developer may assume that every foreign preview is a legitimate share because sharing exists. Inspect the actual recipient, action, expiry, and revocation state for the reported object. A feature-level explanation cannot substitute for the grant that supposedly authorized this response.
Decide whether identifier knowledge is a prerequisite or the flaw
Record how the caller could obtain the target reference, but keep that question separate from whether the action is authorized. In the synthetic example, a report ID might appear in an allowed shared summary. That does not authorize the attachment endpoint to return the full report. The sharing workflow can legitimately disclose the identifier while requiring a narrower action at every later endpoint.
If the report assumes an identifier that the caller has no demonstrated way to obtain, state that prerequisite. It affects practical exploitation difficulty and may guide further investigation. It does not repair a missing object decision in source. Conversely, showing that a report ID is visible does not prove that the preview endpoint lacks authorization.
For a repair ticket, describe the failed rule rather than recommending an opaque identifier as the complete fix. The acceptance condition should require the correct actor, object, action, and projection relationship. Identifier design can remain a separate defense or usability choice, but the source path should enforce the intended permission even when the reference is known.
Keep the finding's scope aligned with the final evidence
If the investigation rejects the preview claim and discovers an unguarded attachment path, create a clearly scoped attachment finding. Link it to the original report as the lead that prompted the review. Do not relabel the old preview observation as if it proved the new path. This matters when the repair owner, data projection, and delivery mechanism differ.
The final internal note can distinguish three useful results: the original source allegation was rejected, the sharing exception was checked for its exact action, and a separate attachment question remains open or supported by its own evidence. That record preserves credit for a useful report while keeping the engineering work precise. It also gives the next reviewer enough context to avoid repeating the same findById alarm without examining the scoped receiver.
For the next IDOR alert, record the actor, object and disputed permission rule, then have the code owner supply the actual store implementation. An external reporter need not have access to your source to raise a useful report. If the disputed object has a parent chain, continue with reviewing parent and child ownership. For the wider tenant review, use the multi-tenant access control method.