ishan.np

Scope encoding: allowlists the gate owns

Authorization answers whether an action is expressible. The previous post argued that answer belongs in a type minted at a boundary the discovery engine does not control. Scope answers a different question: where that action may land. If scope lives as a file the tool reads and interprets charitably, you have documentation โ€” not a rail.

This post is about encoding. How do you represent an allowlist so that an autonomous (or semi-autonomous) pipeline cannot quietly widen it, drift past a program update, or open a side door that never consulted the gate?

Scope that lives as a string next to the scraper is advice. Scope that lives as typed, versioned state the gate alone can mint from is a constraint.

Three failure modes of tool-local scope

Put a scope.json beside the worker and you will feel organized. You will also be wrong in three predictable ways:

  • Drift. The bounty platform updates the program. Your local file is yesterday's truth. The tool still "respects scope" โ€” its copy.
  • Charitable parsing. Wildcards, carve-outs, and "explicitly excluded" subdomains get flattened into a longer allow list. Ambiguity resolves toward more targets because that makes the demo green.
  • Side doors. A new adapter opens its own HTTP client and reads SCOPE from the environment. The proxy never sees the decision. A file that is not on the wire cannot refuse a packet.

None of these require malice. Agents invent call paths. Paths that skip the gate will skip the file you hoped they would read.

Owned state, not shared config

"Gate-owned" is not a slogan. It means three concrete properties of the encoding:

  • Single writer. Scope updates go through the gate (or a loader the gate alone trusts). Tool-local copies are caches at best, never sources of truth. If two processes can rewrite the allowlist, you no longer know which revision authorized a packet.
  • Typed match, not substring hope. Matching is an explicit operation on owned structures โ€” exact host, suffix rule, path prefix, port class โ€” with a defined fail-closed outcome. It is not if host in open("scope.txt").read().
  • Version and lifetime on the handle. The minted capability carries which scope revision and window it was checked against. A later shrink invalidates use at the boundary. Stale tool-local files do not get a veto.

That is the same architectural move as an AuthorizedTarget: make the unauthorized (or out-of-scope) action unrepresentable at egress, rather than "hopefully checked" earlier in the stack.

Encoding shapes that survive contact with agents

Concrete patterns that keep showing up when you take scope seriously โ€” none tied to a particular program:

  • Host class before path class. Separate "which hosts are in play" from "which path prefixes are allowed on those hosts." Mixing them into one glob invites the charitable parse. A host that is out of scope should fail before any path rule is consulted.
  • Explicit carve-outs as first-class rules. Exclusions are not comments in a README. They are deny rules with the same type as allow rules, and deny wins on conflict. If the encoding cannot express "all of *.example except payments.example," operators will approximate โ€” and approximations leak.
  • Action-class narrowing. A host authorized for passive mapping is a different capability from a host authorized for authenticated probing. Scope encoding that only stores hostnames silently upgrades every later stage. Pair the host rule with an action class at mint time.
  • Revision identifiers in the capability. Every minted handle records scope_rev. Metrics and audits can answer "under which revision was this packet allowed?" without reconstructing history from logs.
  • Indeterminate โ†’ refuse. Missing blob, unloadable revision, ambiguous overlap between allow and carve-out: the gate does not guess. Refusal is a counted outcome, not a quiet empty allowlist that looks like "nothing to do."

Fail-closed is a measurement, not a mood

A rail that cannot decide must refuse. That sounds dramatic until you put it on the same scoreboard as coverage:

  • How often did the engine propose a host the gate refused?
  • How does rediscovery change when carve-outs tighten?
  • When scope revisions shrink mid-run, do in-flight handles invalidate, or do you keep spending budget against yesterday's truth?

Those questions only have answers if out-of-scope actions are unrepresentable at the egress boundary. Otherwise you are measuring how often the loader happened to work โ€” not whether the architecture held.

If missing or indeterminate scope becomes a silent pass, your safety metrics measure documentation quality. Prefer a loud refusal you can count.

A lab-shaped sketch (pattern only)

No real program names. No hunt detail. Just the shape:

  1. A discovery worker proposes GET https://lab.example.invalid/health under a program token the gate already accepted for authorization.
  2. The gate does not ask the worker for a scope file. It matches the host against its allowlist revision R17 (lab suffixes only).
  3. Match succeeds โ†’ mint ScopedTarget{host, rev=R17, class=passive_http, exp=โ€ฆ}.
  4. Egress accepts only that type. A parallel worker still holding a string from an old scope.txt beside a scraper cannot call the same egress API.
  5. If R17 cannot be loaded, or the match is indeterminate, minting fails. The refusal is counted. No packets leave for that proposal.

Authorization without scope is a typed blank check. Scope without a gate is a README. Together, at one choke point, they turn proposals into packets โ€” or into measurable refusals.

Where this sits in the series

Earlier posts argued that safety has to be architectural, that the cost of those rails is measurable, that you need a reproducible lab before the numbers mean anything, and that authorization belongs in a type. This post is the companion rail: scope as data the gate owns and encodes explicitly โ€” host class, path class, action class, revision, fail-closed default โ€” not as config the agent interprets charitably.

Next in the build log: how those two rails (authorization + scope) show up in evaluation metrics when the discovery loop is driven against fixtures rather than hope.


This is part of an open build log for my Master's research on safe automated vulnerability discovery. All experimentation runs against self-owned, authorized lab targets only. More about the research โ†’