Skip to content
Melange v0.9.5

Melange v0.9.5

October 17, 2026·pthm
pthm

Melange v0.9.5 fixes an exclusion bypass in generated SQL: a user excluded by a but not relation was allowed through any relation that reached it via an alias or union. It also fixes contextual tuples in the Go runtime reading grants from another schema or another session, list functions that disagreed with check_permission, and commands that exited 0 after a failed write.

Run melange migrate with the v0.9.5 CLI to replace the generated SQL, and upgrade github.com/pthm/melange/melange for the contextual-tuple fixes. Each fixes different issues. A released binary re-applies the changed functions without --force. Tuples need no migration.

Affected configurations

IssueAffected whenEffect
Exclusion bypassA relation reaches a but not relation through an alias or unionExcluded users allowed
Exclusion scopeA union branch and its enclosing relation both have exclusionsValid grants denied
List leaksAs for the bypass, in list_accessible_objects / list_accessible_subjectsExcluded users listed
Contextual tuples: schemaGo *WithContextualTuples, more than one melange_tuples visible to the roleGrants read from another schema
Contextual tuples: concurrencyConcurrent Go *WithContextualTuples callsNon-superuser: SQLSTATE 42501. Superuser: another request’s grants applied
Userset subjects in listslist_accessible_objects for a userset subject on a self-referential or recursive TTU relationObjects missing

The exclusion bypass is confirmed in v0.9.3 and v0.9.4. Earlier versions have not been tested.

Exclusion bypass

type document
  relations
    define owner: [user]
    define writer: [user]
    define blocked: [user]
    define reader: writer but not blocked
    define can_read: owner or reader

With a writer and a blocked tuple for Alice:

CheckExpectedv0.9.4v0.9.5
alice reader docdenydenydeny
alice can_read docdenyallowdeny

Relation closure expanded can_read through reader to writer, so check_document_can_read matched relation IN ('can_read', 'owner', 'writer') and never evaluated reader’s exclusion. define alias: reader alone was enough to trigger it.

Closure expansion now stops at relations with an exclusion (relation, parent relation, or excluded intersection). The enclosing relation calls that relation’s check function.

Exclusion scope

define reader: a but not b
define can_read: reader or c
define final: can_read but not d

A user with b and c should have final. Exclusions from closure relations were applied to the enclosing relation, so reader’s but not b also applied to final and denied them. Each relation now applies only its own exclusions.

List functions

Checks were correct after the fixes above. These list paths were not:

  • Userset and TTU patterns inherited from an excluded closure relation were expanded without its exclusion. Example: reader: [group#member] but not blocked reached through an alias listed blocked group members.
  • The recursive list_subjects strategy dropped inline exclusions under an anti-join optimisation that only the standard strategy emits. Affects exclusions over unions mixing a userset, a TTU, and a direct or implied branch. This predates v0.9.3.
  • X from parent, where the parent’s X aliases an excluded relation, looked up tuples on the parent’s expanded relations and listed excluded viewers.
  • Self-referential usersets (member: [user, group#member] or eligible, eligible: candidate but not banned) listed banned members of nested groups.

Each path now composes or validates through the excluded relation’s list function.

Contextual tuples

*WithContextualTuples creates a temporary melange_tuples view over the permanent relation. The runtime found the permanent relation by querying pg_class, sorting by schema name, and taking the first match.

  • Schema. WithDatabaseSchema("public") could select a_other_dim.melange_tuples. Grants in that schema then applied to contextual checks. This requires the role to have access to both schemas.
  • Concurrency. Temporary views are visible in pg_class to all sessions, so request B could select request A’s contextual view as its base. As a non-superuser B failed with permission denied for schema pg_temp_N. As a superuser B was evaluated with A’s contextual grants.

The configured schema is now used exclusively, temporary relations are excluded, and a missing relation returns ErrNoTuplesTable.

Userset subjects in list_objects

RelationSubjectv0.9.4v0.9.5
member: [user, group#member] or ownergroup:inner#memberinnerinner, outer, top
viewer: [user, group#member] or viewer from parentgroup:g#membernoneroot, child, grandchild
samefolder:root#viewerrootroot, child, grandchild

The self-referential and recursive TTU strategies did not match userset subjects in their base case, and added the subject’s own object after the recursive step instead of seeding it. Both are fixed. In the TTU strategy the subject’s object propagates only if its relation satisfies the inherited relation.

The generated functions branch on whether the subject is a userset, so plain-subject queries are unchanged from v0.9.4.

Write errors

migrate --dry-run, diff, schema pull, config show, status --format json, and expand --recursive / --flatten ignored write errors. Truncated output on a closed pipe or full disk exited 0, so diff --exit-code could miss a breaking change and migrator.Migrate with DryRun could return nil after partial output. Write errors are now returned.

Behaviour change

With WithDatabaseSchema(""), the contextual tuple source is resolved through search_path on each request instead of cached. A configured schema is still cached. The default is public.

Performance

The GitHub-style test schema and the kitchen-sink schema generate the same SQL as in v0.9.4. Only models that reach an excluded relation through an alias or union change.

On the 14 test models whose SQL changed, check and list latency fell 17.9% (geometric mean). Two operations are slower, measured on small fixtures:

Operationv0.9.4 → v0.9.5
list_accessible_subjects with a userset filter, on an alias of an excluded relation~280µs → ~740µs
check_permission on an alias of an excluded relation+10–57%

Both previously returned incorrect results. Kitchen-sink list_accessible_objects benchmarks are unchanged.

Testing

The OpenFGA conformance suite did not catch the bypass: its closest fixture, exclusion_for_some_relations, never added an excluded user.

TestOpenFGA_GeneratedAlgebra generates models combining aliases, unions, intersections, exclusions, and parent inheritance, and compares check_permission with an in-process OpenFGA server across all 128 assignments of seven tuples per model. FuzzAuthorizationAlgebra mutates and shrinks inputs. It found the scope bug.

YAML fixtures cover each fix for check, list objects, and list subjects, with expectations verified against OpenFGA (MELANGE_OPENFGA_ORACLE=1). Go integration tests cover the contextual-tuple fixes, including non-superuser roles and concurrent sessions.

Credits

Nick DiRienzo reported the exclusion bypass, the scope bug, and both contextual-tuple issues, with reproductions and patches, including the differential test.

Try it out

# Install / upgrade CLI
brew install pthm/melange/melange

# Or pull the container image
docker pull ghcr.io/pthm/melange:v0.9.5

# Replace the generated SQL
melange migrate

# Go runtime (contextual-tuple fixes)
go get github.com/pthm/melange/melange@v0.9.5

# TypeScript runtime
npm install @pthm/melange

Feedback

Report security issues through private vulnerability reporting. Open an issue for bugs or feature requests.