The Filtered Answer Is Not the Whole Boundary

Fine-grained access control in a shared database promises a simple thing: let each person see only the rows or fields that belong to them. This series examines the harder question hidden inside that promise—whether private data can influence anything an authorized user can observe before the system removes it from the answer. The distinction matters because a filtered result can look correct while the computation that produced it still carries information about what was filtered out.

The subject is not one product or one bug. It is a design pattern used across relational databases and full-text search systems: multiple users share physical data structures, while a policy is applied to limit the results each user receives. That arrangement can be efficient and convenient. It can also make “not returned” an unsafe synonym for “not involved.”

A row can be absent and still matter

Imagine a table containing records for several clinics. A doctor is allowed to see patients from only one clinic, so the database evaluates a query and removes the other patients before returning anything. From the doctor's perspective, the response appears to contain only permitted rows. But there are two different events inside that sentence. First, the database decides which records match the query. Later, it applies the access policy to decide which matching records may be revealed.

If the private records never affect the first event, the filtering step has a strong isolation story: unauthorized data is outside the computation relevant to the user's answer. If private records do affect that event, the final response may still be empty while the system behaves differently depending on whether a hidden record matched. The difference might appear as elapsed time, a relevance score, an error, or another observable property. The user has not read the row directly. They have learned something because the row participated in work that the policy did not conceal.

This is the central distinction between a result boundary and a computation boundary. A result boundary controls the data placed in the response. A computation boundary also controls whether protected data can change the work, state, or metadata visible through that response. The first is necessary, but it does not automatically imply the second.

The distinction is easy to miss because ordinary interfaces encourage us to inspect the answer, not the path. If a search returns no private document, or a database query returns no unauthorized row, an application test may record a pass. That test has not yet asked whether the hidden document changed ranking statistics, whether the hidden row caused extra policy work, or whether a query planner used a shared index in a data-dependent way. “The record was filtered” describes the output. It does not describe the information available while producing it.

The USENIX Security study that anchors this series makes the issue concrete in PostgreSQL row-level security and in Elasticsearch/OpenSearch document-level security. The mechanisms are different. In the relational case, query execution time can distinguish a nonexistent match from a match that is later rejected. In the search case, corpus-wide scoring behavior can depend on terms in documents that are not returned. The common lesson is narrower and more useful than the claim that all filtering is broken: when rich queries run over shared structures before redaction, the system must be evaluated for signals beyond the response body.

Why shared structures make this design attractive

Shared physical structures are not an arbitrary mistake. Multi-tenant systems use them to consolidate data, standardize administration, and avoid operating a separate database or search index for every tenant. Fine-grained controls can also implement least privilege inside an organization: analysts, services, or applications may need different views of the same underlying data. Centralizing a policy in the database can reduce the chance that every client invents its own check.

Those benefits create the engineering tension. Sharing makes storage and query infrastructure easier to manage, but it means the execution engine sees a larger world than any one user is permitted to see. A policy can remove rows from the returned result without forcing every earlier operation to behave as though those rows were absent. The more expressive the query interface, the more questions a user may ask about that shared world. A simple equality query may expose one narrow membership signal; a range or prefix query can ask about a whole region of values; a conjunction can test whether values occur together. The details belong to later cases, but the architectural pressure is already visible: performance-oriented reuse and strict information isolation are not the same objective.

The boundary also crosses the application layer. Web applications often distribute access checks across handlers, helper functions, and query filters. The OSDI study of Ote describes this as an implicit policy: the code may enforce a rule, but the rule is not necessarily written down in a form that another person can inspect. A team may therefore face two separate uncertainties. It may not know exactly which data queries the application can issue, and it may not know whether the database's execution of those queries exposes signals about data that the application intended to hide.

Ote addresses the first uncertainty by extracting a view-based description of observed application behavior for human review. That can reveal an over-broad permission, an omitted legitimate access, or an access check silently disabled by an interaction elsewhere in the code. It makes the policy easier to discuss; it does not, by itself, prove that every execution path was explored or that shared database computation is free of side channels.

The NIST m-NGAC report approaches the placement problem from another direction. It proposes embedding the NGAC policy model in the database environment, with a mediator intercepting access and an adjudicator evaluating policy. Views can control which fields a SELECT returns, while triggers can authorize modifications. The stated goal is uniform enforcement even when access comes through an SQL editor, reporting tool, application, or other client rather than through one trusted web path.

That proposal is relevant because an access boundary is only as strong as its least-controlled path. But moving policy closer to the data is a design response, not evidence that every observable consequence has disappeared. A database-resident view may preserve a query's filtering logic while masking fields, yet the system still needs validation for timing, errors, score-like metadata, policy-state freshness, administrative access, and the costs of maintaining the enforcement layer. The report describes the architecture and its advantages; it does not supply the same kind of independent quantitative evaluation as the USENIX Security experiments.

The boundary this series will test

The series will follow three connected questions. First, what does the database or search engine do before it applies the policy, and can private data change a signal visible to an authorized user? Second, what policy does the application actually express through its query behavior, rather than through its documentation or a developer's memory? Third, where is the policy enforced, and does that enforcement cover direct connections, alternate tools, reads, writes, and changes to policy state?

The first two questions lead to two different side-channel mechanisms. PostgreSQL shows why optimizer behavior and indexed predicates can turn execution time into a search instrument. Elasticsearch and OpenSearch show why relevance scores and prefix-expanding queries can reveal terms or fragments from filtered documents. These cases should not be collapsed into one generic attack: they require different interfaces, observations, and assumptions. They do, however, test the same proposition—that hiding the final result is not enough if hidden data still shapes visible behavior.

The application-policy question then changes the vantage point. Before an operator can secure a query surface, they need a credible account of that surface. A policy-extraction tool can provide evidence and provenance for review, while its bounded analysis and human judgment remain part of the security story. Finally, database-resident enforcement asks whether the boundary can be made uniform at the place where the data lives. Its promise must be tested against the same full path rather than accepted because the architecture sounds centralized.

A useful isolation test therefore has more than one step:

  1. Identify the queries and operations an authorized principal can cause, including indirect paths through an application.
  2. Trace which protected data participates in planning, indexing, filtering, scoring, errors, and policy evaluation before the response is formed.
  3. List every signal the principal can observe, not just returned values.
  4. Check whether the proposed control removes the dependency, restricts the interface, or merely adds enough noise to make it harder to measure.
  5. Recheck the result when data, indexes, policy relationships, clients, and workload conditions change.

This test does not demand that every shared database be abandoned. It makes the trade-off explicit. Stronger separation may cost more infrastructure; narrower query interfaces may reduce product flexibility; structural indexes or visible-only statistics may impose performance or relevance costs; an auditable policy may still be incomplete. The important judgment is not whether a system uses filtering, but what filtering actually isolates.

flowchart LR A["Authorized user or application"] --> B["Query or search request"] B --> C["Shared database or search structure"] C --> D["Intermediate computation over matching data"] D --> E["Policy filtering or redaction"] E --> F["Returned authorized result"] D -. "may influence" .-> G["Timing, score, error, or metadata"] G -. "observable" .-> A

The next step is to make that path precise: what exactly can an allowed user observe when the system evaluates a query over shared data before applying its policy? Once that question is visible, a missing row can no longer be treated as proof that the row was absent from the computation.

References