What the Database Does Before It Hides a Row

Post-filtering fine-grained access control is easiest to understand as a sequence, not as a checkbox marked “authorized.” The sequence begins with an allowed user query, passes through computation over shared data, and ends with a response in which unauthorized records have been removed. The question here is what information can survive that sequence even when the protected row itself does not. Following the sequence gives the reader a practical way to distinguish a legitimate query from a successful isolation boundary.

The result is produced before it is restricted

Let a database hold a data set D and accept a query Q. Without access control, the query produces some result, written as Q(D). A user's policy P determines which of those records the user may receive. In a post-filtering design, the system first lets the query operate on the shared data and then applies the policy to the result. The simplified picture is P(Q(D)): compute, then remove what the caller is not allowed to see.

That notation describes two different sets. The first is the intermediate set of records that matched the query. The second is the smaller set that crosses the visibility boundary. If a user's query matches three records and the policy permits one, the response may contain one record. The other two are absent from the response, but they were present in the intermediate computation. Whether that matters depends on what the system does while finding, ranking, counting, or filtering those records.

This is not a claim that post-filtering always produces a visible leak. If the protected records cannot change any observable property of the request, then the final filtering step may be sufficient for the particular interface and threat model. The important point is that the response body alone cannot establish that condition. A system must also control, or deliberately evaluate, the work that precedes the response.

Consider two requests that return the same empty result to a user. In the first, no record in the shared data matches the query. In the second, a private record matches, but the policy removes it. From the standpoint of returned content, the requests are identical. From the standpoint of the system, they may involve different numbers of index lookups, policy checks, score calculations, intermediate rows, or error conditions. If any of those differences is exposed through the response, the empty answer is not informationally empty.

The distinction is between a result boundary and a computation boundary. A result boundary governs what values, rows, or documents the caller receives. A computation boundary also prevents protected data from changing the work or state that the caller can observe. A system can have the first without reliably having the second. That is why “the row was filtered” is a description of an outcome, not yet a security argument.

The order of operations is the source of the difference. Filtering the data first would give the engine a query over the permitted subset, conceptually Q(P(D)). Post-filtering instead permits the query to run over the larger set and then returns P(Q(D)). For some simple selections, those expressions may produce the same visible rows. They do not require the engine to do the same work. The abstract model therefore does not say that every post-filtering system is exploitable; it says that the implementation must be checked for information left in the gap between equivalent-looking results. Richer operations can make that gap especially consequential because ranking, intermediate cardinality, or conditional behavior may depend on records that will later be removed.

The boundary can be crossed by more than the response body. Elapsed time is one obvious signal: a request that causes additional work may take longer. A relevance score is another: a result visible to the caller may be ranked using statistics calculated over documents the caller cannot read. Errors, counts, pagination behavior, and other metadata can also carry information when they depend on the hidden part of the data. The relevant question is not whether a signal was intended to be a data interface. It is whether the caller can observe it and relate it to a choice of query.

This also separates a database privilege from a row or document policy. A privilege may authorize a user to submit a class of query at all; the policy then determines which matching records can cross the result boundary. Granting the first does not settle whether the second is safe to evaluate over shared state. A system can correctly deny direct access to a base table while still exposing a permitted search or reporting operation whose internal behavior depends on that table. The security review has to follow the permitted operation far enough to see what it causes, not stop at the name of the object the caller is forbidden to open.

This is why the word query matters. A caller is not merely asking for a fixed object; they may be selecting predicates, ranges, prefixes, combinations of fields, or other conditions. A query interface converts the system into a question-answering surface. If the surface permits only one fixed operation, the number of possible questions may be small. If it permits expressive conditions, a caller may compare the system's behavior under carefully chosen alternatives. The response can then reveal structure even if every individual answer is filtered.

The policy itself is not necessarily a single table of permissions. In the abstract model, it is a predicate over data and the requesting principal. It might express tenant ownership, a clinic or project boundary, a user's relationship to a record, or another condition. The database may also permit writes subject to a policy. The important architectural fact is that the query engine and the policy operate on related state: the policy decides what may be revealed, while the query determines which part of the shared state participates before that decision is applied.

That separation creates an ownership question. The database engine owns the physical structures and the execution plan. The policy owns the authorization condition. The caller controls the query within whatever interface they have been given. A secure design has to account for the interaction among all three. It is not enough for the policy predicate to be correct in isolation if the execution plan can expose how many hidden records it processed. Conversely, it is not enough for a query to be harmless in isolation if the application exposes it alongside a score or timing signal that reveals shared state.

Who gets to ask which questions

The threat model studied in the post-filtering research is deliberately less dramatic than a storage compromise. The adversary is a legitimate user whose access is mediated by the fine-grained control system. They can issue queries from an allowed set and observe the post-filtered results. They are not assumed to bypass the policy, read raw storage, or modify another user's records. Depending on the deployment, they may also be able to insert records under their own authorization. The security problem is whether ordinary privileges provide enough leverage to infer unauthorized data.

This matters because “the attacker cannot access the table” is often used to end the analysis too early. A user may have no permission to select a hidden row directly and still be allowed to submit a query against a table or search index. The query engine can evaluate the request against a larger shared structure before the access policy removes results. The user does not need to cross the policy boundary by force if the policy boundary itself produces an observable distinction.

The paper describes two settings in which the allowed query surface differs. In a direct-access setting, an untrusted principal can submit arbitrary database or search queries within the capabilities granted to their account. They may know the schema and broad bounds on attribute values without knowing the distribution of actual contents. This is a strong environment for testing whether the design isolates computation, because the caller can choose questions that probe the shared structure.

In an app-mediated setting, a trusted application sits between the user and the database. The application translates user actions into database queries, so the caller may attack only through the templates and parameters the application exposes. That is a meaningful restriction. It can remove range queries, prefix queries, visible scores, or write capabilities that a direct client would have. But it does not turn the application into proof of isolation. The relevant audit question becomes: which database operations can this application cause, with which inputs, and which parts of their behavior reach the user?

The difference is easiest to see with a medical-record application. A doctor may be allowed to retrieve patients at one clinic, while a web form exposes a search box and a few filters. The doctor cannot request another clinic's record as a returned object. Yet the form may still cause the database to search a shared patient table. If the interface exposes only a fixed patient identifier lookup, the available questions may be limited. If it exposes ranges over an attribute, combinations of fields, or result metadata, the same policy is being exercised through a much richer surface. The authorization rule has not changed; the information available through its execution may have.

This is also why query expressiveness is a security property, not merely a usability feature. An equality predicate can ask whether a chosen value matches something. A range predicate can ask whether any value lies in an interval. A prefix predicate can ask whether an unknown term begins with particular characters. A conjunction can ask whether several attributes occur together in the same record. Each kind of query can be legitimate for the application's purpose. Together, they can let a caller adapt the next question to the previous response. The later PostgreSQL and search cases show how that amplification works; the abstract lesson is that the number and shape of allowed questions determine how much structure a side channel can reveal.

The same model clarifies what a negative result does and does not establish. “No visible rows” could mean that no data matched, that only private data matched, that a policy condition removed the matches, or that the application suppressed the output for another reason. The body of the response may not distinguish those states, but execution, scores, or metadata might. A security test that checks only the body verifies output filtering. A stronger test compares observables across equivalent visible outcomes and asks whether hidden state changes them.

The model also prevents two opposite mistakes. The first is complacency: assuming a correct policy predicate makes every observable behavior safe. The second is overstatement: assuming that any shared table and any filter automatically create a practical attack. The evidence depends on the query types, indexes, score calculations, write permissions, measurement noise, rate limits, and application path actually available. A narrow app-mediated interface may block the demonstrated strategy; a direct query interface may expose exactly the capabilities the strategy needs. The correct conclusion is conditional, but the condition must be tested rather than presumed.

A useful audit therefore follows the request from the caller to the result. What is the caller allowed to ask? Which data structures answer the question? Does private data participate before policy evaluation? Which quantities can vary because of that participation? Which of those quantities return to the caller, directly or indirectly? And does the answer change when the workload, index, policy, or client changes? These questions apply before choosing a mitigation because they identify the dependency a mitigation must remove.

sequenceDiagram participant U as Authorized caller participant E as Query engine participant D as Shared data structure participant P as Access policy U->>E: Submit an allowed query E->>D: Evaluate the query over shared data D-->>E: Intermediate matches and execution work E->>P: Apply authorization to the intermediate result P-->>E: Permitted subset E-->>U: Filtered result and observable behavior

The next case makes the abstract sequence measurable in PostgreSQL. There, optimizer and index behavior can cause a hidden match to change the amount of policy work even though the matched row never reaches the user. The key question is no longer merely what the policy permits; it is how timing can reveal that the database encountered something the response refuses to show.

References