fix(gates): deny default-tier wildcard bypass of more-specific gated tiers#60
Open
ucekmez wants to merge 2 commits into
Open
fix(gates): deny default-tier wildcard bypass of more-specific gated tiers#60ucekmez wants to merge 2 commits into
ucekmez wants to merge 2 commits into
Conversation
…tiers
`@eep-dev/gates` (TypeScript) and `eep-gates` (Python) let a no-requirements
default tier silently bypass a gated tier through a broader wildcard.
A gate config commonly publishes a broad scope on the default tier and carves
out a narrower, gated path, for example:
default "public": access = ["content.*", "profile.*"]
"paid": access = ["content.premium.*"], requirements = [trust>=20]
Because `content.*` covers `content.premium.X`, `resolveAccess` / `resolve_access`
granted the gated resource via the default tier's broad match even when the
gated tier's requirements were unmet, so a request with no proofs read premium
content (the same class of bypass already patched in the more.md data plane).
The resolver now applies a specificity override: when the winning tier is the
default tier and a gated (requirements-bearing) tier targets the same resource
with an equal-or-more-specific access pattern, the default grant is suppressed
and the request is denied with the gated tier's unmet requirements (a 402
instead of a leak). Behaviour is preserved everywhere else:
- a strictly more specific default pattern keeps its grant (owners can still
open a narrower path explicitly);
- resources no gated tier touches stay public;
- resources the default tier does not cover at all fail closed;
- resource-less resolution and tiers satisfied by valid proofs are unchanged.
Specificity ranks an exact pattern (`a.b.c`) above any scope wildcard (`a.b.*`,
longer prefix wins) above the universal wildcard (`*`). The equal-or-more-specific
(`>=`) tie-breaking matches the deployed more.md Go and TypeScript data-plane
implementations, so re-vendoring the library does not change enforcement.
New exported helpers: `patternSpecificity`, `bestSpecificityFor`,
`defaultTierOverriddenByGatedTier` (TypeScript) and `pattern_specificity`,
`best_specificity_for`, `default_tier_overridden_by_gated_tier` (Python).
Tests: dedicated unit suites in both languages (100% of the changed lines),
plus a shared cross-language fixture
(`tests/parity/gate-resolution-specificity-fixtures.json`) executed by both the
TypeScript and Python suites so the two resolvers cannot drift. The
SPECIFICATION gains a non-normative tier-matching-precedence note; both gates
READMEs and the CHANGELOG document the new behaviour.
Surfaced by the EEP protocol audit.
Signed-off-by: Ugur Cekmez <ucekmez@gmail.com>
The tier-matching-precedence note added in the previous commit was labelled "Informative (non-normative)" but used RFC 2119 keywords (SHOULD / MUST NOT), which is contradictory in an informative note. Reword it as plain descriptive guidance (what the reference implementations do, with an encouragement for custom resolvers) so it carries no normative weight. Signed-off-by: Ugur Cekmez <ucekmez@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes a default-tier wildcard specificity bypass in the access resolver of
@eep-dev/gates(TypeScript) andeep-gates(Python) — Critical task C3from the EEP protocol audit.
A gate config commonly publishes a broad scope on the no-requirements default
tier and carves out a narrower, gated path:
{ "default_tier": "public", "tiers": { "public": { "requirements": [], "access": ["content.*", "profile.*"] }, "paid": { "requirements": [{ "type": "trust", "min_score": 20 }], "access": ["content.premium.*"] } } }Because
content.*coverscontent.premium.X,resolveAccess/resolve_accessgranted the gated resource through the default tier's broad match even when the
gated tier's requirements were unmet — a request with no proofs could read
premium content. This is the same class of bypass already patched in the more.md
Go/TS data plane; this PR brings the fix upstream into the reference library.
What changed
the default tier and a gated (requirements-bearing) tier targets the same
resource with an equal-or-more-specific access pattern, the default grant
is suppressed and the request is denied with the gated tier's unmet
requirements (a
402, not a leak).a.b.c) > scope wildcard (a.b.*, longerprefix wins) > universal wildcard (
*).patternSpecificity,bestSpecificityFor,defaultTierOverriddenByGatedTier(TS) andpattern_specificity,best_specificity_for,default_tier_overridden_by_gated_tier(Python).Why
>=(deny on a specificity tie)The equal-or-more-specific tie-breaking matches the deployed more.md Go
(
internal/eep/gates/http402.go) and TypeScript (api/src/lib/services/gates.ts)data-plane implementations, so re-vendoring this library does not change
enforcement. It is also the fail-closed choice when a publisher lists the same
pattern on both the public and a gated tier.
Testing
vitest run→ 511 passed (newspecificity-override.test.tswith 24 cases +
resolution-parity.test.ts);tsc --noEmitclean; 100% of thechanged lines covered (
resource-matcher.ts100%; the addedaccess-resolver.tslines verified via the coverage JSON).
pytest→ 77 passed (newTestPatternSpecificity,TestBestSpecificityFor,TestDefaultTierOverriddenByGatedTier,TestSpecificityOverrideResolution+test_resolution_parity.py);resource_matcher.py100%, addedaccess_resolver.pylines 100%.(
tests/parity/gate-resolution-specificity-fixtures.json) is executed by bothsuites so the two resolvers cannot drift.
unmet=[trust];content.blog.*andprofile.*stay public).Docs
CHANGELOG.md(Security entry), both gates READMEs (TS + Python), and anon-normative tier-matching-precedence note in
docs/current/SPECIFICATION.md.Stacked on C2 (
fix/auth-adapters-fail-closed); please merge C2 first. Thediff against that base is only this commit.