Vulnerabilities
A Report Reached the Last Desk and Its Column Name Became Python — DIRAC CVE-2026-45579 Reconstructed
CVE-2026-45579 let an authenticated reporting field reach Python eval() while the affected ORM expression was being formed and before SQL generation, turning a narrow RequestManager field choice into code execution under the DIRAC service account.

In this article
1 The night shift begins with an ordinary report field
1.1 One grouping choice reaches a destination no reporting interface promised
An authenticated report request that appeared to ask DIRAC for one grouping column could reach Python's evaluator while the affected ORM expression was being formed, before SQL generation, so CVE-2026-45579 let a low-privileged caller cross from a reporting field into code running as the RequestManager service account.
A night-shift operator opens a Request Management report and asks how many queued records share the same status. The screen offers familiar controls: choose a grouping field, narrow the records with filters, then sort the result. Nothing suggests a programming console; it feels like handing three small cards to a reporting service. That ordinariness is precisely where the danger hides.
The first card might carry Status; a second could narrow the view to an operation Type; a third could say that request identifiers should appear in ascending order. These are product words with modest meanings. They identify a known column, a comparison value and one of two order directions. A sound implementation can turn each choice into an ORM object without granting the caller any authority over Python syntax.
In DIRAC 9.1.9, the final desk did something much larger. Several RequestDB helpers assembled those external strings into expressions and passed them to eval(). Python processed the expression in the service module's environment. If evaluation returned normally, its object was then used to form the affected ORM expression; any Python side effect had already happened independently. The database layer therefore entered the story only after the dangerous decision.
That ordering explains why the case is an eval-injection flaw, not a conventional database-injection story. Escaping quotes for SQL or binding comparison values cannot protect code that Python has already interpreted. The relevant question is not whether the eventual SQL statement uses placeholders; it is who decided which Python object became the grouping, filter or ordering expression before a statement existed.
The card then passes through the public advisory, the handler that accepts the string, and eight Web-query evaluation sites in RequestDB before v9.1.10 separates it into two known models, mapped-column metadata, and two explicit ordering choices. Source explains the defect, regression work shows where a request should stop, and per-worker rollout plus historical review show how the repair reaches a real fleet.
1.2 The public record proves a critical defect, but it does not narrate an intrusion
DIRAC published GHSA-9jpv-c7p4-997x on July 13, 2026 at 14:56 UTC; the record carries CVE-2026-45579. The official record calls the issue Critical, scores it 9.9 under CVSS 3.1, and identifies CWE-95, improper neutralization of directives in dynamically evaluated code. Its vector describes a network-reachable, low-complexity path that needs low privileges and no user interaction, with changed scope and high confidentiality, integrity and availability impact.
The prerequisite matters without making the finding small. The advisory says any authenticated user able to call the affected function could run code or commands as the system identity that operates DIRAC services. Authentication narrows the initial population from the whole internet to identities accepted by the deployment, but scientific-computing platforms often serve distributed communities, automation and federated accounts. A service credential can be less privileged than root and still hold configuration, database access, delegated credentials or valuable network reach.
GitHub's global advisory records three disjoint affected constraints: >= 6, < 8.0.79; >= 8.1.0a1, < 9.0.22; and >= 9.1.0, < 9.1.10. Their corresponding first patched releases are 8.0.79, 9.0.22 and 9.1.10. Those three constraints together define the published affected set; an unsupported historic branch needs migration to a maintained fixed release.
The advisory describes potential full compromise of the DIRAC system and names examples of material a service process may reach, including local configuration, database passwords, stored proxies and tokens. Those are upstream impact statements. They do not establish that every deployment stores the same material in the same place, that every service identity can read it, or that anyone exploited a particular site. Local authority and local evidence must supply those answers.
Four kinds of proof answer four different questions. The advisory establishes severity, prerequisite, affected lineage and impact. Official tag refs show which content-addressed commit objects the named releases resolved to at review time. The v9.1.9-to-v9.1.10 source comparison shows exactly where evaluation disappeared and which helpers replaced it. The upstream tests reveal the security property maintainers chose to preserve.
This distinction stays visible throughout the article. Fixed source proves that the old method evaluated caller-controlled report syntax, and the public, non-executing upstream test proves that __class__ is rejected as an inherited attribute. As of July 18, 2026, the reviewed public record contained no exploited-in-the-wild or site-incident report; that does not prove no incident occurred. Process secrets, egress routes, and log retention still require evidence from the deployment in question.
Tag resolution adds another guard against narrative drift. At review time, the official lightweight tag refs resolved as follows: v8.0.79 to a05a64be9fbf6119080b735ae0a4b2d13cf67a48, v9.0.22 to 8f2dcddb4310790da01299da9cad895423b91669, v9.1.9 to 03de43326b8078cef21ed6405bccebefc003f745 and v9.1.10 to 2f5c5f3b74ab65b3b7d5687d561bc8deee1e4859. Those refs can move, so the full commit IDs anchor this comparison; tag names and short prefixes serve only as locators, and deployment evidence should additionally retain the built artifact digest.
| Evidence object | What it establishes | What it cannot establish alone |
|---|---|---|
| GHSA-9jpv-c7p4-997x | Authenticated prerequisite, Critical 9.9 rating, affected package lineage, fixed versions and stated impact | Whether one deployment was reachable, which secrets its service held, or whether exploitation occurred |
| Official tag refs resolved to commits | The content-addressed commit identities those four refs resolved to at review time | Whether a ref later moves or a running worker loaded that commit |
| RequestDB comparison | Eight removed Web-query evaluations and the exact replacement helpers | Historic requests or process behavior at an operator's site |
| Upstream regression test | Supported reports still work and inherited __class__ is rejected in three paths | Local extensions, vendor backports or mixed-version fleets unless tested separately |
1.4 Behind the dashboard, the RMS hierarchy stores Requests, Operations and Files
DIRAC's Request Management System exists to perform simple operations asynchronously on behalf of users. Its documentation describes failure recovery, data-management work and other extendable tasks. A request can contain ordered operations, and an operation can carry files. Status changes move through that hierarchy so a long-running distributed workflow can survive the moment when the originating client disconnects.
At the center sits ReqDB. It holds Request, Operation and File records, while corresponding Python classes expose mapped properties and behavior. ReqManager publishes useful database queries to ReqClient through DIRAC's DISET service protocol. A Web portal can therefore ask for summaries, counters and distinct field values without speaking directly to the database. In the three affected helpers, that reporting vocabulary resolves only Request and Operation columns; File remains part of the stored RMS hierarchy and regression fixture. The service method is therefore the place where the two-model external vocabulary must become a safe internal query.
The project's documented configuration example places one central RequestManager near the request-processing agents and gives its service authorization a default of authenticated. The security advisory makes that caller requirement concrete for this defect. A deployment may tighten authorization further, but source review cannot assume a local override. Exposure analysis must read the effective configuration and identity mappings instead of treating the example as either a universal grant or a universal restriction.
Three Web-facing database operations matter to the repair. getRequestSummaryWeb() returns rows with filters and ordering. getRequestCountersWeb() groups rows and counts them. getDistinctValues() supplies the choices a portal can display. Their outputs are read-oriented, yet the strings that select their columns still enter a service process with far more capability than a read-only form control implies.
The process and the database have different interpreters. SQLAlchemy knows how to compose mapped columns, comparisons, joins, grouping and order expressions into SQL. Python knows how to resolve names, attributes and calls in its current namespaces. When RequestDB used Python evaluation merely to obtain a SQLAlchemy column object, it connected those two languages with a bridge wide enough for general Python semantics. The reporting feature needed a narrow door and received an interpreter.
With the room mapped, the next step is to follow the external cards from ReqManagerHandler into RequestDB. The handler's type declarations will turn out to be accurate but incomplete: they ensure that a grouping choice is a string and a filter collection is a dictionary. They do not say which string or which keys belong to the product. That missing vocabulary is where an ordinary report request begins to change meaning.
ReqProxy belongs nearby but is not the vulnerable consumer described by the patch. DIRAC documents proxies as failover receivers for new requests when the central ReqManager is unavailable; they serialize work locally and forward it later. The Web reporting helpers in this CVE live at the central manager's RequestDB desk. An inventory still includes proxies because topology, credentials and recovery images affect incident scope, but the public source claim should not imply that every proxy method contains these evaluation sites.
The records are operational instructions, not inert analytics rows. Request operations can replicate, register or remove data and can be executed asynchronously with owner context. The affected Web methods read and summarize that state; they do not directly execute those queued operations. Code evaluation inside the same service process is dangerous because it escapes the read-only semantics of the report path and inherits process authority, not because a counter query itself was designed to modify a Request.
The hierarchy also explains why a reporting query must know its table context. In the affected Web paths, Request status and Operation type come from different mapped objects, and the Operation join can change both row counts and grouping results. File path belongs to the wider RMS hierarchy, but it is fixture data here—not a field model accepted by the current handler or _get_column(). A counting helper that selects a column before fixing the Request or Operation model can land a valid name on the wrong table; one that forgets join cardinality can return a plausible but duplicated total. The repair must therefore keep field resolution within the expected Request or Operation model while preserving the original join, group and distinct behavior under synthetic hierarchy records. A secure rejection path is not enough if the surviving report quietly lies.
2 The string crosses the service interface still wearing an ordinary type
2.1 Handler declarations validate shape, not the reporting vocabulary
In v9.1.9, ReqManagerHandler publishes export_getRequestSummaryWeb(), export_getDistinctValuesWeb() and export_getRequestCountersWeb(). Their type declarations expect combinations such as dictionary, list and integer for a summary or string and dictionary for counters. This protects the service protocol from receiving a wholly different outer object, but a Python string remains capable of holding a legitimate column name, an unknown name or a complete expression.
The counter handler is the shortest route to understand. It receives groupingAttribute and selectDict, documents that the first value names a RequestTable field or the special word Type, and forwards both objects to RequestDB.getRequestCountersWeb(). There is no list of permitted grouping names at the handler desk. The method trusts RequestDB to interpret the product vocabulary correctly.
The distinct-value handler performs one small piece of routing. If the requested attribute is Type, it selects the Operation table; otherwise it selects Request, then calls getDistinctValues(tableName, attribute). That branch captures a real product rule, yet the remaining attribute string stays unbounded. Choosing a known table does not establish that the name belongs to one of its mapped columns.
The summary handler forwards selectDict, sortList, startItem and maxItems to RequestDB. It correctly describes special date keys and the Operation type alias. The route needs that flexibility because the portal can build different reports. Flexibility at the protocol level does not require general expression evaluation; it requires a parser that knows the supported fields, values and directions and returns a normal service error for everything else.
Authorization and input grammar solve different problems. A DISET rule can decide who may call the method. A type declaration can decide whether the serialized outer value is a string. Neither tells RequestDB that Status is a mapped Request attribute or that ASC is an accepted direction. The vulnerability survived because identity and shape checks were followed by a consumer that still treated the string as Python syntax.
The service entrance therefore needs two kinds of validation. DISET can continue to confirm that a parameter is a string, dictionary, list or date. Code close to the data model must then validate vocabulary and relationships: the table comes from a small product set, the field belongs to the mapper's column collection, and direction is one of two stable choices. Moving all model knowledge into the transport would tightly couple protocol declarations to database internals, while omitting the second stage lets any well-typed string pass. RequestDB is the appropriate semantic gate. Failure must occur while the affected ORM expression is being formed, before a matching SQL statement is generated or emitted, return one coherent service error, and avoid exposing internal class names, SQL or a stack trace to the portal.
2.2 Summary filters and sorting assemble Python at the database desk
getRequestSummaryWeb() begins with legitimate query work. It selects Request fields, handles pagination, and joins Operation when the caller filters on Type. Date bounds compare values to Request._LastUpdate. The unsafe turn appears in the generic branch, where a table name and external key are inserted into a string intended to produce a model attribute.
For a list value, v9.1.9 built an expression that included the chosen table, key, in_ method and the list representation, then evaluated the entire string. For a scalar, it evaluated the table-and-key expression and compared the resulting object with the value. The list form is especially revealing: both identifier selection and value representation entered Python syntax even though SQLAlchemy already offered a typed column.in_(values) operation.
Sorting introduced a third evaluation site in the summary path. The first requested sort pair supplied a Request attribute and a direction; code formatted both into an attribute-and-method expression, lowercased the direction and evaluated it. A product with two supported directions had delegated the choice of object and method to a general interpreter. The repair later separates these decisions and recognizes only asc or desc.
Error handling after query execution could not undo this transition. A malformed object might later cause SQLAlchemy to reject an argument, and the method might return S_ERROR, but Python had already parsed and evaluated the string. A security test must therefore observe that invalid identifiers are rejected before a matching SQL statement is generated or emitted, not merely that the HTTP or DISET response ends in an error.
The public upstream case asserts only OK == false and the exact message “Unknown Request attribute '__class__',” while control flow in the fixed source places the resolver exception before the matching SQL executes. Together they locate the rejection order, but they do not supply driver-level zero-send evidence for a particular deployment. To state “no SQL was emitted” as a local measurement, an operator still needs compilation and send counters in an isolated regression and must bind the result to the exact artifact and worker.
The summary sort shape deserves a compatibility fixture of its own. The handler expects a list, while RequestDB uses the first pair as column and direction. Test an empty list, one legal pair, extra pairs according to current product behavior, mixed case in ASC/DESC, an unknown column and an unknown direction. The goal is to preserve the API the portal actually sends and to reject unsupported choices at the resolver, without inventing new semantics during a security backport.
List filters need equally careful data fixtures. Use ordinary strings containing quotes, commas, Unicode and empty values to prove SQLAlchemy receives data objects intact. Record compiled query structure or bind counts, not a database-specific rendering that may vary by driver. These tests show that removing string evaluation does not require mutilating legitimate values. They also catch a downstream fork that reintroduces formatting because a complex list once failed serialization.
2.3 Counters and distinct values repeat the decision five more times
The counter method expands the same pattern across grouping and filtering. It first translates Type to an Operation attribute, Status to Request's private mapped name, and other choices to a Request-prefixed string. It then evaluates the grouping expression when building the selected columns and evaluates it again in group_by(). One external choice therefore crosses the interpreter twice.
Each generic counter filter adds another decision. A list form evaluates a dynamically constructed in_ expression; a scalar form evaluates a model attribute before comparison. Special date bounds remain direct SQLAlchemy comparisons, and Type causes the required join. The mixture of safe explicit branches and unsafe generic branches explains why ordinary testing could pass for years: familiar report keys followed expected paths while the fallback retained a much larger language.
getDistinctValues() contributes the eighth web-query evaluation site found in the v9.1.9 RequestDB file. The handler chooses Request or Operation, the database method normalizes Status, and a string combining table and column enters eval() inside distinct(). A portal may use this method only to populate a dropdown, yet its implementation reached the same interpreter checkpoint as the more visible counter route.
This inventory changes the repair question. “Remove the highlighted eval” is insufficient. The desired property is that every Web report column, filter key and order direction reaches SQLAlchemy as an object selected from a closed grammar. The test surface must cover summary, counters and distinct values because each is a different public consumer even when all three share the same helper after the fix.
Distinct-value calls often happen before a human opens the final report because a portal needs candidate choices for a dropdown. That makes the path easy to overlook in request samples focused on an explicit “Run report” action. Capture page-load and autocomplete traffic in the regression trace, confirm which authenticated backend identity issues it, and include it in historical searches. A read-only convenience endpoint can reach the interpreter through the same route as the main report submission.
| Web reporting path | External choice | v9.1.9 evaluation sites | v9.1.10 replacement | Safe negative check |
|---|---|---|---|---|
getRequestSummaryWeb() | Filter keys, list/scalar values, sort column and direction | 3 — list filter, scalar column, order expression | _apply_web_filter() and _get_order_expression() | Inherited filter key and unsupported direction return S_ERROR before matching SQL is emitted |
getRequestCountersWeb() | Grouping field and selection keys | 4 — selected group, list filter, scalar column, group-by expression | One resolved groupingColumn plus _apply_web_filter() | Inherited grouping name returns unknown Request attribute |
getDistinctValues() | Handler-selected table category; externally selected column | 1 — distinct column expression | distinct(_get_column(...)) | Public path rejects inherited Request columns; helper or backport tests cover unknown tables |
| Total | Three related report grammars | 8 relevant calls | Shared mapped-column check | Positive behavior and three negative consumers tested together |
“Eight” is a reproducible source scope, not the count of every eval() anywhere in DIRAC. The inventory fixes v9.1.9 RequestDB as its object and counts only calls that form column, filter or order expressions in three named Web report methods: three in summary, four in counters and one in distinct values. Grouping counts twice because the same external choice is evaluated once for the selected expression and again in group_by(); v9.1.10 resolves one groupingColumn and reuses it at both positions. Backport acceptance should repeat this method-and-role inventory against built source instead of relying on one repository-wide keyword scan.
Counters add a correctness trap that severity alone can hide. A grouping column should be resolved once and reused as both the selected and grouped expression in a single query. Distinct values must also choose its table and column before constructing de-duplication. If each fragment interprets a string independently, model selection, alias conversion and error handling can drift. The shared resolver turns those decisions into one verified column object. Regression coverage should pair valid grouped totals with empty results, list filters, scalar filters and unknown-field rejection. It should establish that invalid input emits no matching SQL while valid reports keep their previous results, proving that the repair closed the interpreter without changing the numbers administrators depend on.
2.4 A field-by-field custody record shows exactly when ordinary data becomes program text
The handler-to-database handoff becomes easier to audit when each request element has an owner and an expected type at every step. In the vulnerable v9.1.9 object, ReqManagerHandler lines 229–270 define three service entrances. Their type declarations are not fictitious controls: they reject an outer value with the wrong Python container type and they make serialization predictable. The failure is more precise. After those checks, selected strings are still allowed to change category from product data into source code at RequestDB. A useful review record must therefore name both the last component that treats a value as data and the first component that gives it executable grammar.
The summary request carries four materially different objects. selectDict maps external field aliases to scalar or list values; sortList carries a field and direction; startItem and maxItems control slicing after the query returns. The old generic filter branch inserted the key into a model-attribute expression. In the list case it also inserted the textual representation of the list into the expression, while the scalar case evaluated the attribute and compared the returned object with the still-separate value. Sorting inserted both the field and a lowercased direction into a method-call expression. Pagination does not participate in those eight evaluations, but it remains part of resource and compatibility testing because a repaired query that fetches or orders a different row set can make the same slice return different records.
The counter request has a different shape and therefore deserves its own custody row. groupingAttribute first receives a product alias translation: Type becomes an Operation expression, Status becomes the private Request mapping name, and every other string receives a Request prefix. The resulting string is evaluated once as a selected expression and a second time in group_by(). Meanwhile, keys and values from selectDict repeat the generic list or scalar filter behavior. The same user choice can thus be interpreted at two program points even though the result is meant to designate one column. This is why a patch that removes only the first visible call is incomplete, and why the fixed method resolves groupingColumn once before using that single object twice.
The distinct-value request looks smaller but contains an important ownership split. The caller supplies one attribute string. ReqManagerHandler itself chooses the table category: Type selects Operation and every other attribute selects Request. RequestDB then used both strings to form the object passed to distinct(). The table decision is service-owned on the public route, while the column decision remains externally influenced. That distinction matters to testing. An “unknown table” case validates the helper and downstream backports, but the public negative case that follows normal routing should focus on an unknown Request or Operation column. Treating an internal table variable as if it were a directly supplied Web parameter would exaggerate the interface; ignoring the external column would miss the real path.
A concise custody matrix has five columns: serialized location, semantic role, deciding principal, accepted grammar and typed output. For example, a summary filter key is located in a dictionary key, acts as a column selector, is chosen by the authenticated caller, should belong to the Request aliases plus the deliberate Operation Type case, and should become one mapped SQLAlchemy attribute. Its corresponding value is located in the dictionary value, acts as comparison data, is also chosen by the caller, should satisfy the field's type and size policy, and should remain data inside an equality or in_() expression. A sort direction is not a column or value at all; it should become one of two service-owned operations. Writing this matrix prevents a future refactor from applying one generic “sanitize string” function to inputs that require different decisions.
The matrix also exposes authorization questions hidden by mapper membership. A mapped column can be structurally safe to select and still be inappropriate for every authenticated identity. Request fields such as owner or error text may carry more operational detail than a broad portal population needs, and local models may add site-specific data. The patch's column_attrs check answers whether a name identifies a mapped column on one of two models; it does not claim that every mapped column is authorized for every caller or endpoint. Deployments should intersect the structural resolver with a field policy keyed by identity, method and purpose. That is a separate control from CVE remediation, and preserving the distinction avoids misdescribing the upstream patch.
Error custody matters as much as input custody. The old method could raise during Python parsing or evaluation, during ORM expression construction, during SQL generation or at database execution, and broad exception handling could collapse those stages into similar service failures. The fixed resolver produces a deliberate ValueError for an unknown table, column or direction and each public method converts it to S_ERROR. Logs should record the stage as a structured decision without copying an entire untrusted value. A reviewer can then verify that “unknown Request attribute” means the name stopped at mapper membership, while a database exception belongs to a legal query that failed later. A single generic error counter cannot provide that temporal evidence.
Portal architecture can blur the deciding identity. A human may click the report control, while a shared portal backend authenticates to RequestManager under its own certificate or service token. In that arrangement, DISET sees the backend principal, not necessarily the human whose browser selected the field. Exposure inventory must preserve both identities and their correlation record. Tightening authorization only to the shared backend does not reduce which portal users can reach the method unless the portal also enforces its own field and user policy. Conversely, a direct ReqClient caller may present an individual credential and bypass assumptions derived from the Web page. The custody record follows the field through each hop and records which principal was actually authorized there.
Batching, retries and autocomplete add one last operational wrinkle. A single page can call distinct values before submitting a summary, a portal can retry an error through another worker, and scheduled reports can issue the same selection without an interactive user. Correlation IDs should survive those transitions while process identity and serving revision are attached at the final service. During mixed-version rollout, two identical requests may therefore take different code paths. The evidence needed for closure is not merely “the portal returned an error,” but “this caller and request reached this worker revision, stopped at this resolver decision and produced no matching downstream statement.” With custody assigned, the next chapter can examine that sequence at interpreter time instead of guessing from an eventual response.
_get_column().3 The decisive event happens before a SQL statement exists
3.1 eval() consumes syntax before SQLAlchemy receives an object
Python's own documentation is unusually direct: eval() executes arbitrary code, and calling it with untrusted user input creates a security vulnerability. The function parses its source as a Python expression and evaluates it with global and local namespace mappings. When those mappings are omitted, as in the vulnerable RequestDB calls, evaluation uses the environment in which the function is called.
That definition fixes the event order. First, the external reporting string becomes Python source. Second, Python resolves names, traverses attributes and performs permitted expression actions. Third, the returned object is supplied to session.query(), filter(), order_by(), group_by() or distinct(). SQLAlchemy cannot bind, quote or reject a field name until Python has finished the earlier stage.
Prepared statements protect a different stage. They keep comparison values separate from SQL grammar when a database driver sends a statement and its parameters. In the affected list-filter form, even the representation of the value collection was embedded in Python text before SQLAlchemy saw it. A team could have impeccable database parameterization elsewhere and still retain this interpreter defect because the unwanted language is Python, not SQL.
Database permissions still matter to the consequence, but they do not confine the evaluator to database work. The code runs inside the RequestManager process under its operating-system identity. Whatever that identity can read, write, connect to or launch defines the local ceiling. The advisory provides high-impact examples; operators must measure the actual ceiling from service configuration, container policy, filesystem ACLs, environment, credential stores and network controls.
The same timing governs detection. A failed SQL statement is one possible downstream trace, not a required sign of Python evaluation. An expression may fail during parsing, return an unsuitable object, produce a side effect and then fail, or return something the next layer accepts. Logs that record only database errors lose the beginning of the sequence. Request identity, rejected field choice and process telemetry have to meet on the same clock.
This is also why the repair target is not “safer eval.” The reporting feature never needs arithmetic, function calls, comprehensions or attribute traversal supplied by a caller. It needs one model column and, for ordering, one of two methods already chosen by the program. Removing the extra language entirely is simpler to reason about than trying to carve a safe island out of Python's expression grammar.
The default namespace determines the execution context in which this defect operates. RequestDB imports models, SQLAlchemy functions, logging and application helpers into a normal Python module. eval() can resolve names available in that environment, and Python automatically supplies builtins under the documented rules. The advisory explains that attribute traversal can reach command execution. An attacker-controlled attribute expression is therefore evaluated with the service's Python process privileges and access to objects reachable through that namespace; the fixed diff confirms that the repair removes interpretation instead of trying to filter particular strings.
A database account restricted to ReqDB tables could reduce damage from a malicious SQL statement, yet it says little about Python actions outside the driver. Conversely, a locked-down container may limit host impact while leaving mounted application credentials valuable. Model the layers separately: interpreter execution, process identity, container or host isolation, database role, secret mounts and network policy. This produces a concrete impact ceiling and avoids treating one control as a universal sandbox.
The temporal order can be demonstrated with the non-executing inherited name used by the upstream regression test. A laboratory test can place harmless counters at the RequestDB helper entrance, matching SQL compilation and the database send event, then submit that name. The fixed outcome is one helper entry, an ordinary field-resolution error, zero matching compilations, zero sends, and no child task or file change; a valid field should increment the counters normally. That shows rejection before general Python evaluation and before matching SQL compilation or emission, isolating the repair point so that a response-level error cannot be mistaken for proof that evaluation never occurred.
3.2 A mapped class exposes a wider Python surface than its database columns
Replacing eval() with unrestricted getattr(model, external_name) would narrow the syntax but leave the object surface too broad. Python classes carry a namespace and inherit attributes from base classes. SQLAlchemy models add descriptors, relationships, mapper state and application methods. A report selector is entitled to mapped columns, not every attribute the class can resolve.
The difference is visible with the official regression name __class__. It is a normal inherited Python attribute, so broad attribute lookup can find it. It is not a Request or Operation column and has no place in a counter, filter or distinct-value query. The upstream test uses this harmless name precisely because it separates “Python can see it” from “the report grammar permits it” without executing a process or contacting a network service.
SQLAlchemy publishes the metadata needed for the narrower decision. Its documentation explains that inspect(MyClass) returns the class mapper and that Mapper.column_attrs is the mapped-attribute namespace limited to columns and SQL-expression attributes. Relationships and the wider descriptor collection are exposed through separate mapper properties. The application can therefore ask the ORM what it mapped instead of asking Python what the class happens to expose.
This metadata check also survives ordinary schema maintenance better than a duplicated hand-written list. If a supported mapped column is renamed, added or removed, mapper inspection reflects the effective model in the running build. The public API may still need aliases and deprecation rules, but the final lookup cannot silently drift into methods or inherited internals. A local extension can further restrict the set when business policy exposes fewer columns than the model contains.
There are two allowlists in the fixed helper, and both are necessary. A dictionary permits only the Request and Operation model names. Mapper inspection then permits only a mapped column on the selected model. If code checked columns while allowing an arbitrary module global as the model, or fixed the model while accepting any Python attribute, one half of the decision would remain open.
The report card can now be described without metaphor. Its external alias is parsed into a model identifier and a column identifier. The model identifier is selected from two constants. The column identifier is normalized through a tiny alias map, checked against column_attrs, then retrieved with getattr(). The returned value is a SQLAlchemy mapped attribute; no caller-supplied expression is evaluated at any point.
column_attrs is intentionally narrower than attrs or all_orm_descriptors. A relationship may be mapped and useful in application code, but selecting it as a scalar report column can change joins and cardinality. A hybrid property can execute custom Python or emit a complex SQL expression. The fixed helper chooses the namespace documented for column and SQL-expression attributes, matching the objects these reports already used; a stricter product policy can then exclude sensitive or unsuitable mapped expressions.
Membership must be checked on the resolved alias, while error reporting should retain the public name. For Status, the mapper knows _Status, but clients know Status. Testing only the external spelling against metadata would break a supported field; returning only the private spelling in an error would leak implementation detail and confuse portal maintainers. The helper's two-name handling preserves both schema correctness and a stable client vocabulary.
3.3 Four clocks separate evaluation, ORM construction, SQL compilation and database send
The phrase “before SQL” is accurate but too coarse for testing unless the sequence is divided into observable events. The first clock is Python parsing and evaluation of the constructed expression. The second is SQLAlchemy receiving the returned object and incorporating it into a query. The third is compilation, when a dialect turns the ORM expression tree into statement structure and bind information. The fourth is the driver send or execution event. A legal request normally advances through all four. A fixed request with an unknown column should stop before the first clock exists at all: the application performs ordinary dictionary selection and mapper membership, raises ValueError, and never asks Python to interpret caller-supplied source.
Python's documented eval() contract explains why the first clock cannot be treated as harmless preparation. It parses an expression and evaluates it with the supplied global and local mappings; when the caller omits them, the function uses the calling environment under the language's rules, including access to builtins. Expression evaluation can resolve names, traverse attributes and invoke operations before returning a value. The RequestDB call sites were not using a parser limited to SQLAlchemy identifiers. They asked the Python runtime to produce an object from text in a normal application module. The fixed code never needs to prove that a particular string is dangerous, because the product has no reason to offer that language in the first place.
The returned object marks a second, independent decision. SQLAlchemy accepts class-bound mapped attributes and expression objects to build a query tree. If Python evaluation returns such an object, query construction may continue and the final SQL can look entirely ordinary. If it returns an unsuitable object, SQLAlchemy may reject it later. Neither result changes what already happened during evaluation. This is why a database audit can show a normal report statement, a failed statement or no statement at all after the interpreter was reached. The SQL trace describes the second through fourth clocks; it does not reconstruct the first by itself.
Compilation and send must also remain separate in a rigorous test. SQLAlchemy can compile an expression without opening a network connection, and an engine hook can observe a statement before the database accepts or executes it. A fixed unknown-field case should ideally record zero compilations for the corresponding report and zero driver sends. A legal case should record the expected query family and bind shape. If an instrumented test records compilation but no send for an invalid field, rejection happened later than the resolver property described by the patch. If it records neither but an evaluator hook fires on a legacy worker, the absence of SQL has not established safety; it has only located the failure earlier.
Exception timing gives incident responders a practical classification. A syntax or name-resolution error during evaluation may surface as a RequestDB exception without a database event. An ORM argument error appears after an object returns from Python but before or during compilation. A driver or database error appears after a statement has been prepared or sent. Fixed unknown-column and unknown-direction choices take a fourth, deliberate path: a narrow resolver decision becomes normal S_ERROR. Preserve the exception class internally, the service method, the serving revision and the last reached stage. The client can receive a stable message while investigators retain enough structure to decide which clock moved.
A capability ledger translates the first clock into local impact without inventing a site compromise. Begin with the effective operating-system or container identity of RequestManager. Record readable configuration and credential locations, writable application and temporary paths, executable and service-control permission, database roles, mounted sockets, outbound destinations and delegated credential APIs. For each capability, note whether it existed during the vulnerable interval and whether independent telemetry can observe its use. The official advisory supplies examples of sensitive material a DIRAC service may hold; only this local ledger can say which examples apply to a particular worker.
Keep capabilities and observations in different columns. A process able to read a database password establishes potential access, not evidence that a caller read it. A child-process event, file-open record or outbound connection following a correlated request may strengthen an execution hypothesis, but still needs identity, timestamp and host context. Conversely, the absence of an observed event is meaningful only if the relevant sensor was active, clocks were synchronized and retention covers the interval. This disciplined language lets a Critical code-execution flaw remain appropriately serious without converting possibility into a fabricated forensic conclusion.
These four clocks also guide production monitoring after repair. Unknown-field rejections on a fixed worker should end at the resolver and have no corresponding report statement or process anomaly. A legal report should proceed to a known SQL family without spawning a process or touching unrelated secrets. Any event that contradicts this sequence—an unknown name followed by compilation, an evaluator call in a report method, a serving revision outside the approved set or a process effect correlated with the request—deserves immediate triage. The model is useful precisely because each observation has one place in time; it replaces the vague question “did SQL injection happen?” with a checkable account of what the service actually did.
4 The old branches reveal the small grammar the product actually needed
4.1 Type, Status and date bounds are deliberate API aliases
The vulnerable code was not wholly unstructured. It already knew that a public grouping choice of Type meant Operation.Type, while most choices belonged to Request. It knew that public Status mapped to the private ORM name _Status. It knew that ToDate and FromDate were upper and lower comparisons against Request._LastUpdate. Those branches define the real product language.
Preserving that language matters because a security repair must not quietly break the reports operators use to run a distributed service. A portal should still group operations by type, count requests by status, restrict a time window, fetch distinct values and order summary rows. The safe implementation changes how names become objects; it does not ask users to write table-qualified Python or abandon existing aliases.
Type demonstrates why a flat list of Request fields would be incomplete. The value lives on Operation and requires a join in the summary or counter query. The program must choose both the model and the join behavior. The caller supplies the public word, while the service owns the mapping to Operation.Type and the query topology that makes it meaningful.
Status demonstrates a different mapping. The public API uses a friendly name, but the imperative SQLAlchemy mapping binds it to Request._Status. The v9.1.10 helper keeps an explicit alias dictionary, resolves the private name, checks that name in mapper metadata, and then retrieves the column. Compatibility is preserved without permitting arbitrary underscore-prefixed attributes.
The date keys are operators disguised as field names. ToDate means “LastUpdate is earlier than this value,” and FromDate means “LastUpdate is later than this value.” They deserve explicit branches because neither is a database column that should pass through generic equality filtering. Naming the operator in program code makes the time semantics visible in review and test fixtures.
A practical API contract can now list every category: ordinary Request columns approved for reporting, the Operation Type alias, the Status name translation, two date-bound operators and two order directions. Unknown fields fail closed with an error. New reporting capability arrives through a reviewed mapping and tests, not by enlarging a fallback expression language.
4.2 Columns, comparison values and directions need three different parsers
Dynamic query builders become safer when they stop calling every input “a string.” A column name selects an object from schema metadata. A comparison value remains data carried into a SQLAlchemy expression. An order direction selects one of two application-owned operations. These inputs may share one serialized request, but their grammars, output types and rejection rules are different.
For identifiers, the output should be a mapped attribute such as the object returned by getattr(Request, resolved_name) after metadata membership succeeds. Quoting an identifier string is a fragile substitute because database dialects differ and Python evaluation occurs earlier in this case. Returning a typed ORM object lets the library own SQL rendering and makes it impossible for punctuation in the original name to become a second statement fragment.
For values, the fixed path carries the original Python value into column.in_(value) when it is a list or column == value for a scalar. SQLAlchemy then handles binding under its normal rules. Dates keep their explicit comparisons. The value is never converted into a piece of Python source, and the filter helper does not need to understand the textual representation of strings, lists, numbers or timestamps.
For ordering, the program lowercases the requested direction, checks membership in {asc, desc}, and invokes that known method on the already approved column. A direction is not a suffix to paste after an external attribute. If a future product needs null placement or another ordering mode, it can add a named enum case with a behavioral test and known SQLAlchemy operation.
4.3 A report-field workbook preserves meaning while the dangerous language disappears
Removing evaluation closes the vulnerability, but a mature backport also needs to preserve what each accepted report word means. The most useful artifact is not merely a flat allowlist. It is a field workbook with one row per public alias and columns for model, resolved ORM attribute, permitted methods, comparison shapes, required join, output label, visibility policy and representative tests. Status, for example, resolves to Request._Status, can participate in summary filters and counters, needs no Operation join and should keep the public label Status. Type resolves to Operation.Type, changes query topology and must be tested against Requests with zero, one and several Operations.
Model and join belong together because a column name alone cannot determine correct results. A valid Operation field used from a Request summary requires the relationship path that makes Operation rows available. If one Request has two matching Operations, the query's grouping and selected columns decide whether the Request appears once or twice. The source already carries an explicit join and group set for Type; the workbook records that as part of the public meaning, not as an incidental implementation detail. A future resolver that returns the right column but omits or duplicates the join would be safe from eval injection while still giving administrators false totals.
Comparison shape is another independent contract. A scalar value uses equality, a list uses in_(), and the date aliases choose less-than or greater-than comparisons against Request._LastUpdate. The workbook should state whether an empty list means no matches, an error or a skipped filter; how null values behave; which timestamp zone and precision the service accepts; and whether numeric and textual forms are normalized. These decisions may be inherited from current behavior rather than introduced by the security patch, but writing them down prevents a backport from “fixing” evaluation by silently changing reports that operations teams use for queue and failure decisions.
Output semantics matter as well. The summary response contains named parameters, stringified records and a total count, while counters return a mapping and distinct values return a list. A legal-field regression should compare not only OK but parameter order, record representation, count type, stable pagination and empty-result shape. If the repaired query returns the same data under a different key or converts a timestamp differently, a portal may fail even though the resolver is secure. Positive fixtures are therefore part of the security deployment: they keep pressure to restore the old dynamic shortcut from appearing after an avoidable compatibility outage.
Field visibility should be written as a second selection after structural resolution. The upstream helper admits mapped columns on Request or Operation; a site can define a smaller set per service method and caller group. The workbook can mark a field as mapped but not portal-visible, available to an operations group but not a broad user group, or prohibited from distinct-value enumeration because enumeration itself reveals sensitive values. This policy should return an ordinary authorization or unsupported-field result without exposing whether a hidden column exists. Keeping it outside the mapper test preserves a clean distinction between “is this a column?” and “may this identity use it here?”
Names require a documented normalization policy. The fixed sort helper deliberately lowercases direction, but column lookup remains case-sensitive apart from the explicit Status alias. Trimming whitespace, folding Unicode or accepting punctuation variants could make different inputs converge on one field and complicate audit trails. The workbook records the submitted form and canonical form for every supported transformation. Unknown spellings fail closed. If a portal wants friendly localization, it should translate display labels into stable wire aliases before the request leaves the client; RequestDB should not guess natural-language variants in the data layer.
Version the workbook with the service contract. Adding, renaming or removing a mapped column is not automatically an API change, because the public set can remain stable through aliases and policy. Conversely, changing a public alias or join can break clients even if the SQLAlchemy model is untouched. Release notes should identify the wire-level change, deprecation interval, affected methods and migration test. During a mixed-version deployment, clients should send only the intersection supported by all serving workers, or routing should pin newer vocabulary to newer workers. An unknown-field error caused by version skew is operational evidence, not a reason to restore expression evaluation.
Local extensions are the place where the workbook earns its cost. A virtual organization may subclass or replace RequestDB, add mapped attributes, expose custom portal reports or change authorization. Source review should locate those overrides in the final package and compare their workbook rows with upstream. A local method that still formats a model name, attribute or operation into eval() is not repaired because the upstream file is clean. A local method that uses unrestricted getattr() may also exceed the mapped-column contract. Build-time inventory should therefore follow call paths and capabilities, then run the same harmless inherited-name rejection against each reachable consumer.
Resource policy completes the grammar. A field can be safe and authorized but expensive to group, sort or enumerate across a large Request database. Set maximum list length, page size, time window and request rate; identify columns that require an index; cap or disable distinct-value enumeration where cardinality is excessive; and measure legal query plans on the supported database engine. These limits should fail with explicit service errors and telemetry, not time out after tying up workers. They do not mitigate Python evaluation, but they ensure the typed replacement remains operable and does not invite teams to bypass it for performance.
The workbook finally becomes a review script. For every row, the editor or engineer can point to the public alias, exact mapper object, join, comparison, authorization decision, positive fixture, harmless rejection and expected SQL family. For every input absent from the workbook, the expected result is a resolver or policy rejection before matching SQL. This is a much smaller object than Python's expression language and a much richer object than a list of strings. It preserves product meaning, gives every team a shared description of the expected behavior and gives future maintainers a concrete place to add capability without reopening an interpreter.
5 The repair turns external names into typed ORM objects
5.1 _get_column() binds two known models to their mapped columns
The v9.1.10 comparison adds inspect to RequestDB's SQLAlchemy imports and places a new static helper near the beginning of the class. _get_column(table_name, column_name) has one job: return a supported ORM column attribute without evaluating the input. Keeping that conversion in one small function makes the security decision reusable and reviewable.
Inside the helper, models maps two supported, service-owned table categories to program-owned classes: Request and Operation. Public handler paths choose that category inside the service; the external choice is the field name. “Unknown table” is therefore most directly exercised by helper-level and backport rejection tests. A lookup miss raises ValueError, and no caller can make RequestDB search other module imports or invent a dotted path.
A second dictionary maps the public name Status to _Status. All other column names keep their spelling. The helper tests the resolved name against inspect(model).column_attrs, the mapper collection limited to mapped column and SQL-expression attributes. A miss raises another ValueError that identifies the table category and original external name.
Only after both checks succeed does the helper call getattr(model, resolved_name). At that point getattr() is a retrieval mechanism, not the policy. The explicit model dictionary and mapper membership test have already proved that the result belongs to the required object class. This ordering is what makes the use of dynamic attribute access appropriate.
The helper's error messages are part of the operational interface. “Unknown table” distinguishes a caller trying an unsupported model category from “Unknown Request attribute” or “Unknown Operation attribute.” A portal can render a normal failure, a regression test can assert the exact rejection point, and monitoring can count new field choices without logging an exception trace for every malformed report.
Downstream forks should preserve the complete decision if they backport the patch. Copying the final getattr() line without model and mapper checks recreates the broad surface. Replacing column_attrs with a wider descriptor collection also changes the guarantee. Review the helper, imports, call sites and tests as one object, then record the source commit and built artifact digest used by the deployment.
The model dictionary is a useful choke point for future review. If File-backed reporting is later added, placing File in that dictionary would make its mapped columns technically resolvable, but it would not automatically authorize them for every method. The change should identify required joins, exposed fields, identity policy and result-cardinality tests. Treating dictionary membership as an explicit design event keeps schema growth from silently widening the Web API.
5.2 Filter and order helpers keep values and operations in narrow lanes
_apply_web_filter() is a class method because it begins by calling the shared column resolver. It receives an existing query, a known table category, an external column name and a value. If the value is a list, it returns query.filter(column.in_(value)); otherwise it returns an equality filter. No string formatting is required in either branch.
_get_order_expression() performs a parallel conversion for sorting. It resolves the column first, lowercases the direction and checks that the result is either asc or desc. Any other value raises ValueError. The method then invokes the approved ordering method on the mapped column and returns the resulting SQLAlchemy expression.
Resolving the column before checking direction does not expose evaluation; both steps are closed selections. It can, however, influence which error a caller sees when both inputs are invalid. Regression tests should lock the service behavior that local clients depend on, and monitoring should treat either unknown-column or unknown-direction results as rejected input without assigning malicious intent automatically.
The public methods retain explicit date and Type branches. Summary and counters still join Operation where required and still compare date bounds directly. Their generic filter branches now call _apply_web_filter(); summary ordering calls _get_order_expression(). The patch reduces repeated construction code while preserving the topology of legitimate queries.
Value validation still belongs above or beside the filter helper. A mapped integer, timestamp or enum may reject a value at serialization, application or database time, and very large lists may create resource pressure even when they are safely bound. Set type, size and pagination limits according to the report contract. The CVE repair removes code evaluation; it does not promise that every syntactically safe report is cheap, meaningful or authorized.
Ordering tests should examine stable pagination as well as rejection. A report sorted by a non-unique column can move records between pages when rows change, so existing code may rely on a secondary key outside this patch. Capture current output with duplicate values, minimum and maximum page sizes, and both directions before backporting. Security fixes are easier to deploy when operators can show that familiar reports retain their row set and ordering guarantees.
5.3 Normal S_ERROR responses close all three report paths coherently
The patch changes every affected public helper, not only the counter route described in the advisory's shortest example. Summary filters call the new filter helper, summary ordering calls the order helper, counters resolve their grouping column once and reuse it for selection and grouping, and distinct values pass the result of _get_column() into distinct(). The eight evaluations disappear from these Web query paths.
Each method catches ValueError and returns S_ERROR(str(e)). DIRAC clients already understand the S_OK/S_ERROR convention: a success carries OK and Value, while an error carries OK=false and Message. Invalid fields therefore become ordinary service rejections, with no need for a Python traceback, database exception or special transport status.
Counters now resolve groupingColumn exactly once. Type chooses Operation.Type; every other public choice is resolved on Request, including the Status alias inside the helper. The same object enters both session.query() and group_by(). This removes duplicate interpretation and makes selection and grouping structurally identical.
Distinct values also gain a meaningful table check. The handler still routes Type to Operation and other public attributes to Request, while RequestDB verifies that the chosen table category is recognized and the column is mapped. RequestDB no longer assumes that a handler's branch alone made a complete authorization decision about Python attributes.
A source grep provides a useful but limited release check. In v9.1.9, eight eval() calls appear in the three Web reporting methods; in the v9.1.10 fixed RequestDB paths, those constructions are replaced by the helpers. The broader repository may contain evaluation for unrelated, separately designed features. A scanner should tie findings to reachable untrusted input and expected grammar before declaring them equivalent to this CVE.
The result is a compact repair with a larger architectural meaning. External report names are no longer mini-programs. They are requests to select one object from a service-owned vocabulary. The next proof must show both halves: legitimate reports remain useful, and a name inherited from Python but absent from the ORM schema is rejected consistently by summary, counters and distinct values.
| Input category | Service-owned grammar | Typed output | Fixed-build rejection |
|---|---|---|---|
| Model | Request or Operation | Known ORM class | Unknown table ValueError becomes S_ERROR |
| Column | Resolved alias present in inspect(model).column_attrs | Mapped SQLAlchemy attribute | Unknown model attribute |
| Scalar value | Application data type and field policy | Bound equality expression | Normal validation or database type error, never Python source |
| List value | List accepted by the selected field | column.in_(values) | Normal typed rejection, never formatted into an evaluator string |
| Order direction | asc or desc | Known SQLAlchemy ordering expression | Unknown sort direction |
Coherent errors must still serve operations. The portal should receive a stable, localizable unknown-attribute or unknown-direction result. Service logs can retain a request correlation identifier, method, normalized field category and worker identity without copying the entire untrusted value. That gives support staff enough material to distinguish a typo, an old portal and deliberate probing while avoiding a second injection surface in logs. Monitoring may count rejection rates by method and caller identity, using the pre-upgrade baseline for thresholds. A single error is not exploitation evidence, and even a sustained pattern only starts an investigation; authentication, process and host records must still support any conclusion.
5.4 Three maintained lines need the same invariant, not the same line number
The advisory's three fixed versions are release objects on distinct maintenance lines. Their exact content identities at review time are v8.0.79 a05a64be9fbf6119080b735ae0a4b2d13cf67a48, v9.0.22 8f2dcddb4310790da01299da9cad895423b91669 and v9.1.10 2f5c5f3b74ab65b3b7d5687d561bc8deee1e4859. These commits are useful immutable views even when their subject line concerns release notes or a dependency pin: the tree attached to each commit is the source object being reviewed. A mutable tag name, package filename or branch head is a locator; the full commit plus the built artifact digest is evidence of content.
The 9.0.22 and 9.1.10 trees place the three resolver helpers at RequestDB.py lines 192–221 and the same range. The 8.0.79 tree carries the equivalent block at lines 194–223. The two-line shift is unimportant; the decisions are the same: select Request or Operation from a program-owned dictionary, translate Status, require mapper column_attrs membership, pass values directly to SQLAlchemy and restrict direction to asc or desc.
A backport should be evaluated by that invariant rather than by patch shape. Older branches may have different imports, class layout, SQLAlchemy behavior, result helpers or formatting, so a byte-for-byte transplant may fail or be wrong. An acceptable implementation can rename the helpers or structure them differently if every external model and column choice ends in a finite, mapped selection; values remain values; order operations remain finite; and all eight semantic positions in summary, counters and distinct values consume those typed objects. Conversely, a patch that applies cleanly but leaves one dynamic group-by expression is incomplete.
A backport begins with the target branch's own imports and mappings. Confirm that inspect(model) returns a mapper, that column_attrs contains the fields its reports actually use, that public Status resolves to the correct private mapping, and that Request and Operation bind to the intended model objects. If an older SQLAlchemy release exposes different inspection behavior, adapt the implementation without widening it to relationships, every descriptor, or arbitrary class attributes. Unrestricted getattr() is not a compatibility layer; it abandons the property the repair is meant to establish.
Once mapping is sound, follow every consumer: generic list filters, scalar filters, and ordering in summary reports; the grouping column, list and scalar filters, and grouping expression in counters; and the selected column in distinct values. Count semantic roles rather than occurrences of the word eval, because a downstream line may move interpretation into a helper or substitute another execution primitive. Local RequestManagerHandler and RequestDB overrides inside the final wheel or image belong to the same path. The acceptance record should name exact locations and roles; a repository keyword total cannot prove that reachability is gone.
The path ends at the success and failure contract seen by clients. Unknown Request fields, unknown Operation fields, unsupported tables, and invalid directions should become stable service rejections without stack traces or corresponding database statements, while legal aliases retain response structure, joins, counts, and ordering. If the target branch keeps an older S_ERROR convention, validate the serialized result the portal actually receives. An uncaught exception from an otherwise safe helper may block execution yet create an outage and invite a hurried rollback, so compatibility is itself a condition for keeping the repair deployed.
The underlying fix history makes that branch proof more precise. On the 9.1 line, commit 628700c68564a723a147feb2e374fe5aa97832c4 added the positive report matrix and three inherited-name assertions while RequestDB still carried the vulnerable implementation. Its direct child, 8ec11072f48103d94e8b7cca831e2c7ce304d6c7, removed evaluation from RequestDB. A later commit, 88501cdcb851bad352193e5e3b2e3e0f0ffa79d7, changed formatting and Pylint placement in the relevant file. This order is valuable evidence: the security assertion was written against behavior that initially failed, then the repair made it pass.
The parallel fix commits are a6f75ce5d369254d46ca6f1d39e3f384366fa396 on the 9.0 line and 99f1af88b525e69a77f204aebf7d8df3b6056142 on the 8.0 line. At the reviewed release objects, 9.0.22 and 9.1.10 contain the identical repaired RequestDB blob, while their handler blob is also identical to v9.1.9. That combination isolates the architectural change: the transport-facing method shapes stayed stable and RequestDB became the semantic gate. Blob identity is strong source evidence; it still does not show which tests a release job ran or which file a live process loaded.
The test snapshots supply an important qualification. The published test_web_queries_reject_unknown_attributes() case is present in v9.1.10, but the v9.0.22 release snapshot retains the older RequestDB test blob even though its production RequestDB blob is repaired. The v8.0.79 test snapshot likewise lacks the new case. The accurate statement is therefore that v9.1.10 carries the public regression, v9.0.22 carries the identical fixed implementation, and v8.0.79 carries an equivalent branch-native implementation. Operators must run the rejection matrix themselves on 9.0 and 8.0 artifacts; the existence of a test on 9.1 cannot be lent across branches as execution evidence.
Branch-native positive data matters especially on 8.0. Its Request mapping exposes fields such as DIRACSetup and OwnerDN, while the reviewed v9 summary includes Owner. A mechanical copy of the v9 fixture can therefore fail on a legitimate v8 schema difference and tempt someone to weaken the resolver. Construct valid cases from the destination branch's own mapped columns, preserve its public aliases and joins, then reuse the security assertion that inherited attributes stop outside column_attrs. The invariant transfers; the business vocabulary and line numbers need not.
6 A safe laboratory proves where the request stops
6.1 Official tests contrast real mapped records with inherited __class__
The v9.1.10 regression fixture uses an in-memory SQLite database wired through RequestDB's real SQLAlchemy mappings. It creates one Request named for the Web summary, adds a RemoveReplica Operation targeted at a synthetic storage element, attaches one File with a test path, and writes the object hierarchy through putRequest(). File makes the stored hierarchy representative but is not queried by the affected helpers; the fixture's security value comes from exercising the real Request and Operation mappings, joins and error propagation.
The positive summary case filters on Type, orders by RequestID ascending and expects one record. The positive counter case groups by Type, filters Status as Waiting and expects one RemoveReplica count. The positive distinct-value case asks Operation for Type and expects that same value. These assertions protect behavior users need after the security change.
The negative half uses __class__ in three positions. Summary receives it as a filter key, counters receive it as the grouping attribute, and distinct values receive it as a Request column. All three results must have OK set to false and carry the exact message “Unknown Request attribute '__class__'.” The string is visible to Python classes but absent from mapped column metadata.
This is a well-chosen security fixture because it tests the decisive rejection without carrying an executable payload. It would expose a repair that blindly switched to broad getattr(), yet it does not spawn a process, read a file or initiate a connection. Downstream suites can add other harmless inherited or unknown names, empty strings, mixed case, unknown table categories and unsupported order directions.
The official suite establishes the intended source behavior; it does not prove every live worker loaded that source. A long-running service can retain an old module in memory after files on disk are upgraded. The laboratory therefore needs a second dimension: two isolated workers whose package contents, process start times and observed responses make the difference between installed code and running code visible.
6.2 Two isolated workers reveal the gap between a package and a running process
Build the regression environment with synthetic records, no production configuration, no delegated credentials and an egress-deny policy. Place two disposable RequestManager workers behind a test-only router. One represents the pre-fix artifact for source-level comparison; the other loads the intended fixed build. Label their process IDs, image digests and start times in the test ledger, not in the request data.
The old worker never needs a command-bearing expression. Legal fields demonstrate baseline report behavior, while a harmless inherited name and instrumentation show that the legacy route reaches its evaluation function before failing later. Stub process creation, filesystem writes and networking at the operating-system seam. The test asks where control flows, not whether a dangerous action can succeed.
The fixed worker receives the identical positive matrix and harmless negative names. Supported Request columns, the Operation Type alias, Status, both date bounds and both sort directions must preserve response shapes. Unsupported model, column and direction choices must return S_ERROR before general Python evaluation, matching SQL compilation or emission, or any side-effect instrument fires.
Now update the package files visible to the old worker without restarting it. Its process start time and loaded module behavior should remain old, even though a shell inspecting the filesystem sees the repaired version. After draining and restarting that instance, the rejection response should change to the fixed helper message and its loaded source digest should match the release artifact. This exercise catches a common rollout illusion.
Repeat the test through the same protocol and routing layer used by staging. A direct unit call proves RequestDB logic, while a DISET or portal path proves serialization, handler forwarding, identity context and load balancing did not alter the test. Attach a correlation identifier at the gateway and carry it through service logs, SQL observation and process telemetry.
Instrumentation must fail safely on the legacy worker. Patch or hook only inside the disposable environment, make every process, filesystem and network primitive return a recorded denial, and run under an unprivileged temporary account. The harmless inherited name should be enough to demonstrate that control reached the evaluator. If the harness unexpectedly requests a real resource, stop the case, preserve its trace and correct the isolation before continuing.
Artifact provenance can be tested in the same exercise. Pull the candidate from the registry used by deployment, verify signature or digest, start it without mounting a developer checkout, and ask the running interpreter for the module file and revision through a controlled diagnostic. Compare that evidence with the build manifest. This catches mutable tags, layered images that retain an old package and startup scripts that select a different virtual environment.
6.3 Request, SQL and process telemetry must tell one synchronized story
At the request layer, retain timestamp, authenticated identity, source address, session or token identifier where policy permits, service method, grouping alias, filter-key set, sort field and direction, result status, duration, correlation ID and serving revision. Do not copy secret-bearing comparison values into a security lake merely to investigate field selection. Key names and value types usually provide the needed signal.
At the application layer, distinguish parser rejection from downstream failure. Fixed builds should emit structured unknown-table, unknown-attribute or unknown-direction decisions with the same correlation ID. Legacy traces may contain Python or SQLAlchemy exceptions. The advisory notes that one use can leave an exception printout in the RequestManager log, but an absent traceback cannot exclude execution, so exception search is only one evidence stream.
At the database layer, capture statement fingerprints and bind counts from the isolated test, then define a privacy-aware production baseline. A rejected identifier on a fixed build should compile, emit and execute no corresponding reporting statement. Legal requests should produce stable query families with expected joins. Raw values and full SQL text may expose user or file information, so fingerprints and selected metadata are often sufficient.
At the host or container layer, correlate RequestManager events with child-process creation, executable mapping, shell invocation, unusual file reads or writes, access to configuration and credential paths, outbound connections, service restarts and log alteration. The exact telemetry source may be auditd, eBPF, systemd, runtime events or an endpoint agent. Coverage and retention dates belong in the case record.
Field punctuation, dunder-style names, call-like characters and repeated rejection bursts can prioritize review, but they are not automatic proof of exploitation. A scanner, misconfigured portal or future legitimate field can create a strange name. Conversely, a carefully chosen expression might not resemble a simple signature. Detection should join identity, method, field novelty, response, process behavior and deployment revision.
After rollout, unknown-attribute events become a durable control signal. Alert on repeated attempts, new callers, high-value service identities and any rejection followed by process or network activity. Keep a lower-severity engineering view for one-off mistakes so developers can find stale clients. The same telemetry then supports both incident response and regression monitoring without declaring every malformed report hostile.
Clock quality determines whether the join is credible. Record UTC timestamps with sufficient precision, monitor NTP health on gateways, services, database hosts and sensors, and retain original event time plus collection time. When clocks differ, document the measured offset and search a widened interval. An apparent request-to-process sequence can reverse if one host drifts, while an overly narrow join can hide the real pair.
Protect the logs from the account under investigation. The advisory notes that local logging may allow evidence removal if the service identity controls the file. Forward security-relevant RequestManager events to append-only or separately administered storage, monitor gaps and unexpected truncation, and keep collection health outside the service host. A missing local line then becomes a recorded gap, not the only copy of the event.
| Case | Expected service result | Expected SQL | Expected process effect |
|---|---|---|---|
| Request mapped column | S_OK with preserved report shape | One approved query family | None beyond normal service work |
Operation Type | S_OK with required join | Approved Request–Operation join | None |
Status alias | S_OK | Mapped _Status predicate | None |
FromDate/ToDate | S_OK | Explicit LastUpdate bounds | None |
Inherited __class__ | S_ERROR unknown Request attribute | Zero matching queries | Zero |
| Unknown table | S_ERROR unknown table | Zero | Zero |
| Unsupported direction | S_ERROR unknown sort direction | Zero ordered query | Zero |
| Stale live worker | Legacy behavior identifies failed rollout | May differ from fixed expectation | Remove from routing, preserve evidence, restart |
6.4 Three independent oracles keep a familiar error page honest
A defensible regression verdict comes from three observations that cannot substitute for one another. The response oracle records whether the service returned the expected S_OK or S_ERROR shape. The query oracle records which matching report statement was compiled, sent and executed. The capability oracle records whether the process attempted an unrelated filesystem, process, credential or network action. A fixed-looking error page answers only the first question. Legacy code can return an error after reaching evaluation, and a test harness can accidentally suppress database work while missing another process effect. All three oracles need the same correlation ID, artifact and worker identity.
Begin observation only after the fixture is ready. The upstream v9.1.10 test creates tables and writes a synthetic Request, Operation and File hierarchy before it exercises the reporting calls; those setup steps legitimately generate SQL. Warm the connection, finish fixture insertion, confirm the expected records exist, reset evaluator, compilation, driver-send and capability counters, then assign a fresh correlation ID to one report case. Otherwise, fixture traffic can make a correct rejection look as though it queried the database, while a stale counter from another case can make a legal report look unsafe. Store the reset event itself so another reviewer knows exactly where measurement began.
The three consumers need distinct query oracles because their fixed control flow is not identical. In summary, lines 700–748 create the base SQLAlchemy query before a generic filter reaches the resolver at line 740; execution through .all() occurs at line 748. An invalid filter can therefore coexist with an already-created query object while still producing no driver send. Counters resolve an invalid grouping field at lines 800–804 before constructing the counter query at lines 806–808. Distinct values evaluates _get_column() as an argument at line 852, so a resolver failure prevents that query call from completing. “No ORM object” is not a valid shared assertion; “no matching driver send for the rejected report” is.
Before trusting a zero, replay the three positive cases from Section 6.1 and confirm the service response, expected join or query family, binds, row count, and absence of unrelated capability events. A compilation listener that sees nothing on a legal query gives no meaning to its zero on rejection; if worker or correlation metadata disappears before the database event, the statement cannot be assigned to the current case.
Then replay the three __class__ contrasts from the same section and require the exact unknown-Request-attribute result from summary, counters, and distinct values. Scope database events to the correlation ID and report family; connection health checks, transaction bookkeeping, and unrelated background statements do not belong to the rejected report. Keep unsupported direction and a helper-level unknown table as separate properties, without pretending the latter is a direct public table parameter.
Define “no SQL” in the result record rather than using it as shorthand. The strongest portable observation is zero matching cursor sends or executions for the rejected report. Zero compilation is an additional laboratory result only when a reliable compilation listener was installed, positively exercised and scoped correctly. Query-object creation, expression construction, compilation, cursor send and database execution are separate events. The fixed-source control flow establishes rejection before corresponding execution; a measured zero at earlier stages belongs to the local test, not automatically to every deployment or SQLAlchemy version.
The capability oracle can remain safe without an execution expression. In the disposable legacy worker, instrument the exact evaluator reference used by the loaded RequestDB module so entry increments a counter and immediately denies further action. At the operating-system seam, deny and count process creation, filesystem mutation and outbound connection attempts. Use only the inherited name from the upstream test to identify legacy evaluator reachability. The fixed worker should not reach that counter. Validate the hook against a benign, isolated unit fixture first, because patching a different imported symbol can produce a false zero while the module keeps its original local reference.
Pair rejection with a wider correctness matrix. Create one Request with several Operations, repeated Operation types and another Request with no Operation; test empty results, scalar and list filters, quotes and Unicode inside data values, both date bounds, both order directions, duplicate sort values, minimum and maximum page sizes and stable pagination. The resolver can be secure while a backport changes join cardinality, aliases, binds or ordering. Recording those outputs protects the reporting behavior operators depend on and removes pressure to reintroduce dynamic construction when an inadequately tested fix reaches production.
Run the matrix against the artifact deployment will consume, not a developer checkout. Retain package or image digest, full source revision, loaded module path and digest, Python and SQLAlchemy versions, database adapter, process start time and routing identity. For v9.1.10, the published test at lines 47–78 provides the positive and inherited-name baseline. The v9.0.22 tree carries the identical repaired RequestDB blob but its release snapshot does not carry that new test; v8.0.79 carries an equivalent branch-specific repair and different valid model fields. Those two lines therefore need their own branch-native test run instead of borrowing the existence of the 9.1 test.
Give every case one reproducible result row: worker identity, artifact digest, case name, service response, evaluator entry count, matching compilation count when measured, cursor-send count, capability-event count, legal SQL family and result cardinality. Attach raw events and observer configuration, then have another engineer replay at least one success and one rejection. The laboratory can prove one artifact on one worker; it cannot count the copies serving production. Its final row should therefore feed directly into the fleet ledger, where routing state and per-process identity decide how many times the proof must be repeated.
7 The fleet investigation begins after the unit test turns green
7.1 Inventory versions, identities, endpoints and process authority together
Start with a reproducible inventory, not a screenshot of one package command. List every central RequestManager, development instance, experiment-specific deployment, disaster-recovery environment, container replica and dormant image that can return to service. For each entry record hostname or workload identity, endpoint, region, owner, environment, package source, version, source revision, image digest, Python environment and process start time.
Compare each installation with the three advisory constraints: >= 6, < 8.0.79; >= 8.1.0a1, < 9.0.22; and >= 9.1.0, < 9.1.10, whose first fixed points are 8.0.79, 9.0.22, and 9.1.10. The earlier code-and-response map pins those release objects to full commits; the fleet ledger records the source and artifact identity actually deployed at each site. A vendor backport remains acceptable only when a documented source diff and tests prove the same RequestDB property.
Version inventory and reachability belong in the same row. Record DISET and Web-facing routes, load balancer pools, internal proxies, firewall paths and administrative tunnels. An endpoint absent from a public DNS list may still be reachable from a broad research network or automation plane. A service disabled in configuration but present in an old image can reappear during rollback or recovery.
Next enumerate identities that could call the reporting methods during the vulnerable window. Include individual certificates or accounts, DIRAC groups, federated mappings, robot identities, service tokens, portal backends, scheduled reporting jobs and retired collaborators whose access may have lingered. Preserve both authorization configuration and identity-provider history; today's group membership cannot reconstruct who was authorized six months ago.
For every service process, document the operating-system account, container user, readable configuration, environment variables, mounted secrets, database role, delegated credential access, writable paths, local logging destination, service-control permission and outbound network reach. This is the authority an evaluated expression would inherit. Do not copy secret values into the inventory; record secret identifiers, locations, owners and rotation procedures.
Finally define the vulnerable interval per instance. It begins when an affected artifact first became active, not when a ticket was opened. It ends only when the last request-serving process loaded fixed code and old replicas could no longer receive traffic. Package installation time, image publication time, pod creation, process start, load-balancer membership and rollback history are separate clocks; the evidence table should keep all of them.
Discover instances from several independent inventories. Configuration services may know named RequestManagers; orchestration platforms know running workloads; registries know deployable images; certificate or DNS records reveal older endpoints; network telemetry can reveal callers reaching an address absent from the current manifest. Reconcile these lists by stable workload identity and owner. An unexplained endpoint is an investigation item even when its version cannot yet be queried.
Keep client and server evidence distinct. A portal package may be fully updated while its configured RequestManager URL points to an older central service, and a fixed central service may still be reached through a stale proxy or DNS alias. Record the endpoint actually selected by each major client population, then trace it through routing to serving processes. This prevents a correct client version from becoming false assurance about server-side evaluation.
7.2 Software state, exposure state and incident state answer three different questions
The fleet ledger becomes useful when it drives a decision instead of ending as a spreadsheet. Classify every row on three independent axes. Software state is affected, fixed upstream, semantically equivalent backport or unknown. Exposure state is reachable, deliberately withdrawn or not yet proved. Incident state is evidence found, no indicators observed within a named scope or unresolved. One reassuring fact cannot close all three. Installing fixed code changes software state; a narrow authorization rule changes exposure; a historical search changes only the incident assessment.
A stock artifact within one of the three published affected intervals requires permanent upgrade or a validated backport whenever it serves, or may serve, a reporting route. The initial record needs the exact version and digest, routes and caller populations, an owner and a repaired-artifact plan. Waiting for a suspicious request is not a reasonable decision rule for a network-reachable Critical flaw whose caller prerequisite is ordinary authentication. The rollout can be scheduled according to local availability constraints, but the software row remains open until fixed code is actually loaded.
An affected artifact that is disabled or believed unreachable has a different exposure state, not a different code state. Keep all routes withdrawn, verify method authorization and network controls, block the artifact from rollback and recovery, and repair it before reactivation. “Not in public DNS,” “portal button hidden” and “no traffic seen” are not equivalent to positive route closure. A defensible unreachable decision includes a configuration snapshot, routing membership, authorization policy and a controlled denied attempt from a designated identity through every known ingress.
An official fixed release, or a later build known to carry the same repair, satisfies the software decision only after every worker loads it. Record per-process artifact digest, module revision, start time, routing interval, one legal report and the harmless rejection result. A fixed package sitting on disk while old workers remain alive is a mixed fleet, not closure. If the load balancer cannot identify which process answered, keep the instance row unresolved; one fixed response at an aggregate address cannot testify for its neighbors.
A vendor or local backport stays provisional until it demonstrates semantic equivalence. The proof includes all eight query-building roles removed, the Request/Operation model choice, mapper column_attrs membership, direct scalar and list value handling, finite ordering, ordinary service errors, branch-native positive behavior and a finished-artifact digest. A version suffix such as “patched” or a ticket that cites the CVE supplies no such evidence. Local RequestDB or handler overrides must be reviewed in the built package even when the upstream base version is fixed.
A version outside the advisory's described lineage also needs careful language. A maintained release newer than a fixed point can be accepted when its ancestry or source shows the repair. A much older, unsupported release that sorts before the affected range is not automatically proved safe; it may have different code, no security support and no meaningful comparison with current tests. Ask the maintainer for an assessment or migrate to a supported fixed line. “Outside the published constraint” describes database classification, not a universal safety guarantee.
| Observed state | Repair decision | Evidence required before closure |
|---|---|---|
| Affected stock artifact serves or may serve reporting | Upgrade or validate a backport now | Digest, routes, caller set, owner and repaired-artifact plan |
| Affected artifact is withdrawn | Keep withdrawn; repair before reactivation | All routes denied, policy revision, blocked rollback and recovery copies |
| Official fixed code is loaded by every worker | Software repair satisfied; historical review remains separate | Per-worker revision, start time, legal report and harmless rejection |
| Fixed files on disk but workers are stale or mixed | Not closed | Routing membership and per-process restart plus behavior evidence |
| Vendor or local backport claimed | Provisional | Eight roles, typed resolver, branch tests and artifact digest |
| Fixed base version contains local overrides | Version alone is insufficient | Override call-path and final-package review |
| Artifact, endpoint or worker identity unknown | Open remediation row | Owner, deadline, containment state and classification evidence |
Reachability should be modeled as routes, not a yes-or-no column. Include browser-to-portal-to-DISET, direct ReqClient, scheduled report jobs, internal administrative addresses, legacy DNS or certificate aliases, management tunnels and recovery environments. Each route names its source population, authentication handoff, method set, target pool and serving-process evidence. ReqProxy belongs in topology and recovery inventory because it can reveal stale paths, credentials or images, while the reviewed evaluation sites remain in central RequestManager RequestDB. This keeps network accounting complete without assigning the vulnerable functions to the wrong component.
Identity needs the same two-ledger treatment. One ledger records the human or automation subject at ingress, certificate or token identifier, DIRAC group and virtual-organization mapping, validity interval and portal session. The other records the identity that DISET actually authorized, which may be a shared portal backend. Preserve the correlation between them. Today's group membership cannot reconstruct yesterday's access, so retain certificate validity, token issuance, group changes, retired collaborators and portal audit events with their effective dates.
Incident state begins only after the vulnerable interval is defined per process. It starts when an affected artifact first enters routing and ends when the last route to the last process closes or fixed code is loaded. A clean search of well-scoped logs can support “no indicators observed” for that population and interval; it cannot repair the artifact or prove another unlogged route was unreachable. Evidence of a correlated process effect changes the incident response, not the source version. Keeping the three axes separate produces precise language and prevents a fixed deployment from erasing the need to understand its past.
Unknown is an actionable state, not an invitation to assume the most convenient answer. Every unknown artifact, endpoint, identity mapping or worker receives an owner, deadline, temporary containment status and the exact evidence needed to classify it. High-value or broadly reachable unknowns move first. When the information arrives, update only the axis it answers. This discipline gives the next subsection a clear job: temporary controls can reduce exposure while repaired artifacts are built, but they never rewrite the software state recorded here.
7.3 A temporary control earns its name only after every alternate route is tested
Temporary response follows a strict priority. Withdraw the affected report methods wherever operations can tolerate it. Where they must remain available, reduce the caller set to named identities with a documented need. Then reduce RequestManager process authority and improve evidence collection while the repaired artifact is built. Each action buys an accountable maintenance interval; none changes the affected source. The exception record should say “temporarily contained pending fixed code,” not “mitigated” without an expiry, owner or test.
Method-level authorization is the clearest control when the deployment supports it reliably. Apply policy to summary, counters and distinct values separately, because a portal may call the last method during page load even when the user never submits a report. Test one permitted and one denied identity through each method, preserve the effective configuration revision and record the response at the central service. A rule checked only in the browser interface does not control direct ReqClient or another portal backend. If policy cannot express these distinctions consistently, withdrawing the reporting route is more dependable.
Every alternate ingress needs an explicit test. Include the visible portal, direct DISET or ReqClient access, shared backend calls, internal load-balancer addresses, legacy DNS and certificate aliases, scheduled-report identities, administrative tunnels, retry paths and the disaster-recovery environment. Use a designated account and harmless legal request to confirm the allowed route, then a designated denied account to confirm rejection before RequestDB. Record which worker and policy revision answered. An unused path is not closed merely because it did not appear in recent traffic.
A shared portal backend requires special care. If that service credential is the principal DISET authorizes, limiting RequestManager to the backend simply moves the decision one hop upstream. The portal must retain the human identity, enforce its user and field policy, prevent direct reuse of the backend credential and produce a correlation record connecting browser subject to service call. Otherwise every portal user may still reach the affected methods under the one allowed certificate. This architecture can reduce network exposure while leaving the advisory's authenticated prerequisite fully satisfied.
An application-aware gateway can add a second filter only if it understands the decoded canonical request structure. Validate exact public field aliases, value shapes, order directions, list length and page size against the portal contract; reject unknown keys before forwarding and log the normalized decision. Test alternative serialization and encoding forms accepted by the real stack. A device that scans raw bytes for underscores, punctuation or Python words is not validating the product grammar and can fail differently across JSON, DISET or logging representations.
Punctuation blacklists and rate limits remain secondary controls. A blacklist tries to recognize fragments of a general language that the feature never needed, while alternate representation or an unanticipated expression can evade it. Rate limiting reduces noisy probing and expensive report bursts, yet one authorized request can still reach the evaluator. Keep both controls if they serve detection and availability, but do not use their presence to move the software row from affected to fixed. Their success criteria are measurable rejection and reduced volume, not elimination of the vulnerable call sites.
Consequence controls must also be specific. Deny outbound destinations RequestManager does not need and verify the denial from its actual runtime identity. Remove unused secret mounts, writable directories, package-management permission, service-control access and host sockets; narrow the database role to required operations; forward security logs beyond the service account's ability to alter them. For every retained capability, record the business function that needs it and the sensor that can observe use. A generic claim of least privilege cannot replace this before-and-after inventory.
Do not let containment destroy the historical record. Before restarting suspect workers, rotating credentials or deleting temporary files, preserve routing state, process and network information where warranted, service and identity logs, policy revisions and the affected artifact. Hash collected objects and record clock offsets and retention. Forward new rejection and route events during the maintenance interval. If the only local log belongs to the same service identity, copy it to separately administered storage before relying on it to support a no-indicators conclusion.
Every exception needs an owner, start time, expiry, methods and routes covered, permitted identities, policy digest, test results, monitoring query and exit criteria. Re-test after configuration, identity mapping, portal or load-balancer changes because those can reopen a route without touching RequestDB. An expiring control should page its owner before it lapses; silently extending it turns an emergency measure into undocumented architecture. If business pressure demands a wider caller set, record the decision and accelerate deployment rather than weakening the rule invisibly.
Exit only on code and process evidence. Every serving worker and standby must load a fixed upstream release or validated backport; legal reports must preserve results; inherited fields and invalid directions must stop in the typed resolver; rollback and recovery artifacts must carry the same repair; and temporary authorization or gateway exceptions must be removed or converted into ordinary, justified policy. Compare routing and rejection telemetry before and after removal. These controls buy time to perform the next rolling replacement carefully; replacing the interpreter path is what ends them.
7.4 Build once, drain old workers, restart and attest the code that serves traffic
- The deployment gate begins with one reviewed source object and one immutable artifact. Resolve the intended lock or package metadata, run the upstream and local regression suites, generate a component inventory, and attach source revision, build job identity and digest. Inspect the finished wheel or container to confirm RequestDB contains the three new helpers and the affected Web methods no longer construct expressions with
eval(). - Stage that same artifact in an environment that uses representative DISET routing, authentication, ReqDB schema and SQLAlchemy version. Run the full legal and rejection matrices, including pagination, joins, date bounds and both order directions. A green helper unit test cannot substitute for a portal request that crosses serialization, handler forwarding and the database adapter used by the planned release.
- Roll out with explicit instance accounting. Remove one old worker from routing, allow or terminate in-flight reporting calls according to service policy, replace or restart it on the immutable artifact, then record its new process ID, start time, image digest and health evidence before returning it to the pool. Continue until the pool contains no old process. A simple package upgrade on shared storage does not satisfy this sequence.
- During mixed-version rollout, route the harmless inherited-name canary deliberately and record which revision answered. Do not spray unknown fields across production users or treat a load-balanced error as proof that every replica is fixed. Health metadata or an authenticated diagnostic should expose serving revision and process start time so the canary can be tied to one worker without revealing sensitive configuration.
- Rollback artifacts need the same scrutiny. If the previous known-good operational image contains the vulnerable RequestDB, an automatic rollback can reopen the interpreter path minutes after a successful upgrade. Promote a repaired rollback candidate, test it, and make deployment policy reject any artifact whose provenance or RequestDB invariant is missing. Disaster-recovery images and cold standby hosts belong to this gate too.
- Release completion requires four matching facts for every traffic-serving instance: the approved artifact digest is installed, the process started after that artifact became available, a legal report passes, and an unsupported inherited field returns the fixed
S_ERRORbefore matching SQL is emitted. Keep the result with the deployment record. Aggregate uptime and a version banner cannot replace per-instance proof.
Schema changes are not central to this patch, yet the release plan should still verify ReqDB compatibility before traffic moves. Run the candidate against a restored synthetic or sanitized schema snapshot, confirm query plans and roll-forward procedures, and separate application rollback from database rollback. If a broader release contains migrations, the repaired RequestDB artifact must remain available through the entire transition so operational rollback does not require a vulnerable code image.
Define abort criteria before the canary begins: any interpreter call from a report path, any rejected identifier that emits SQL, any mismatch between loaded and approved revision, unexpected report cardinality, or material latency regression. The operator should know which evidence to capture and how to drain the instance. A prewritten abort path shortens exposure and prevents pressure during rollout from turning an anomalous signal into an undocumented exception.
| Release gate | Required evidence | Failure response |
|---|---|---|
| Artifact | Fixed-stream version or reviewed backport, source revision, build identity and immutable digest | Block promotion |
| Loaded code | Three helpers present, eight Web-query evaluations absent, process started on approved artifact | Drain and restart the worker |
| Behavior | Legal matrix passes; inherited and unknown choices fail before matching SQL is emitted | Keep instance out of routing |
| Fleet | Every pool member and standby has individual revision and test evidence | Continue instance accounting |
| Rollback | Fallback artifact carries the same invariant and has its own digest | Disable vulnerable rollback |
7.5 Review history and rotate credentials according to evidence
Preserve evidence before aggressive cleanup. Export relevant gateway, DISET, RequestManager, database, identity-provider, host, container and network records with their time zones, retention windows and collection hashes. Snapshot volatile process and network state where suspicious activity or an unexplained legacy worker exists. A rushed log deletion can destroy the very joins needed to decide which credentials require emergency rotation.
Search the vulnerable intervals for summary, counter and distinct-value calls from each authorized identity. Prioritize unknown or inherited field names, expression-like punctuation, repeated errors, unusual timing and callers that rarely use reporting. Then correlate those events with service exceptions, SQL fingerprints, child processes, file access, configuration reads, outbound traffic, queue changes, authentication changes and log gaps.
Build hypotheses that can fail. One hypothesis may state that a strange field came from a stale portal and produced no process activity; another may state that an authenticated call preceded a new child process and outbound connection on the serving host. Write the evidence expected under each explanation, test both, and record which facts remain unavailable. A suspicious string alone is weaker than a synchronized request-to-process sequence.
If evidence indicates code ran in the service context, isolate the affected instance, preserve volatile and disk evidence, and rebuild it from a trusted artifact. Rotate DIRAC configuration secrets, database credentials, stored or delegated proxies and tokens, automation credentials and any downstream material the process could read. Perform rotation from a trusted environment, invalidate old values, and verify consumers adopted the replacements.
If logs are incomplete but a high-value secret was exposed to the process throughout the vulnerable window, a precautionary rotation may still be justified. Record that decision as risk treatment, not proof that the secret was stolen. Conversely, do not declare every credential in the organization compromised merely because the advisory is Critical; tie scope to the service account's actual authority, mounts, configuration and observed behavior.
Notification and closure decisions should state what is known, inferred and unresolved. The advisory contains no public claim that this deployment was exploited, and local source review cannot answer the question. A defensible incident record says which populations were searched, how long telemetry was retained, which indicators were found, what containment occurred and why remaining uncertainty is acceptable or still assigned to an owner.
Historical search should account for representation changes. A gateway may log JSON-escaped keys, DISET may serialize Python values, an application message may truncate a field, and a security product may normalize underscores or punctuation. Build search fixtures from synthetic traffic through each retained layer, compare the recorded forms, and search for both exact safe indicators and structural anomalies. Document blind spots so a clean search result is not granted more confidence than the logs support.
7.6 Credential rotation follows process reachability, not the headline score alone
The advisory names configuration, database passwords, stored proxies and tokens as examples of material a DIRAC service may reach. Those examples establish plausible high impact; they are not an inventory of every installation. Build the local decision from the RequestManager runtime identity, its mounts and configuration, credential brokers, open file or memory behavior and downstream trust. Do not copy secret values into the case record. Use stable credential identifiers and locations so investigators can discuss reachability, issue replacements and audit old use without spreading the material they are trying to protect.
Give every credential one row with an operational owner, issuer, storage location, whether the service identity could read or request it, whether it existed during the vulnerable interval, memory and disk cache behavior, privilege, lifetime, downstream consumers, historic audit source, revocation mechanism and expected outage cost. Add the artifact and worker population that could expose it. A database password mounted only on one central RequestManager is a different decision from a delegated proxy fetched on demand for many tasks, and both differ from a portal backend certificate that never enters the service process.
Four decision states keep the response proportional and reviewable. Observed access or use correlated with suspicious execution requires emergency revocation and replacement. Material readable during the interval when telemetry cannot determine access normally justifies a documented precautionary rotation. Material demonstrably outside the service identity's reach needs no CVE-driven rotation, but the reachability proof is retained. Unknown reachability receives an owner and deadline; rotation priority then follows credential value, privilege, lifetime, trust breadth and the cost of waiting, rather than silently treating unknown as safe or compromised.
Rotate from a clean administrative environment, never from a RequestManager host whose integrity is in question. Confirm issuer control and emergency contacts, create replacement material, distribute it through the normal protected channel, update a controlled consumer, and observe authentication failures, queue latency and service health. Expand to remaining consumers, revoke the old value, then issue a negative check that proves old authentication fails. If execution evidence exists, rebuild affected workers from trusted artifacts before giving them replacement secrets; patching the same process and handing it new credentials is not recovery.
Database credentials interact with connection pools and transaction behavior. Inventory every RequestManager and administrative consumer of the role, create or rotate to a least-privileged replacement, update connection configuration, drain or restart pools according to the driver, and monitor failed logins plus queue processing. Revoke the old password only after intended consumers have moved, then search database audit logs for use of the old identity or credential after the cutoff. If the role itself accumulated unnecessary grants, correct them as a separate authorization change and test report plus request-processing behavior.
Certificates, delegated proxies and tokens have issuer and expiry semantics rather than database-pool semantics. Record subject, audience, scope, validity, delegation chain, refresh mechanism and every trust store or service that accepts them. Revoke or deny the old identifier at the issuer or relying parties, update automation and portal services, and test both a permitted new flow and a rejected old flow. A short natural expiry may reduce residual lifetime but does not prove a copied token was unused; historic issuance and use logs still determine what can be concluded about the vulnerable interval.
Configuration secrets and automation material often persist in more places than the running container reveals. Search orchestration secrets, deployment variables, configuration services, backup snapshots, cold standbys, disaster-recovery systems, developer support bundles and rollback images by identifier and version. Update the sources from which future workers will start, not only the current mount. A primary fleet can pass every fixed-code test and later recover a vulnerable image carrying an old credential unless artifact and secret baselines move together.
Caches make revocation observable only after their lifetime is addressed. Identify in-memory clients, local files, sidecars and proxy caches that can continue presenting old material; clear them through controlled restart or cache invalidation, then watch for attempts using the retired identifier. Preserve denied attempts because they show which consumer was missed, but avoid restoring the old value merely to silence alarms. Define a limited rollback for availability that uses the repaired code and a separately approved credential path; a rollback that reactivates both vulnerable source and revoked secrets compounds the incident.
Close each row with four facts: replacement issued from a trusted environment, intended consumers demonstrably use it, the old value is revoked or expired beyond acceptance, and post-cutoff monitoring finds either no accepted use or an investigated exception. Record residual gaps and the date at which supporting logs age out. “Rotation ticket complete” is an administrative status, not this technical proof. Where a secret was readable but no incident evidence exists, describe the action as precautionary risk treatment; where access or misuse is observed, preserve the stronger incident finding and its supporting chain.
Engineering and incident leadership sign different conclusions. Engineering attests that repaired artifacts and reduced capabilities now serve traffic. Incident leadership records the vulnerable interval, sources searched, credential decisions, notification obligations, unresolved evidence gaps and reopening conditions. The same person may coordinate both workstreams, but one signature cannot replace facts missing from the other. That separation lets the final chapter close honestly: typed report fields prevent recurrence in code, while process and credential records explain what was done about the period before that repair.
8 Keep Python out of every future reporting grammar
8.1 Give the last consumer a complete decision and close with reproducible proof
The reusable lesson is smaller than a ban on dynamic reports. Users can still choose fields, filters and ordering at runtime, while the program first translates the request into a small typed representation: a model from constants, a column from the intersection of mapper metadata and product policy, comparisons and ordering from finite enums, and values that remain data. The database adapter consumes only that representation, never an arbitrary expression. A new alias or operation arrives with accepted cases, rejected cases, serialization compatibility and result-cardinality fixtures.
Keep authorization in the design without asking it to perform input parsing. A caller may have permission to view Request reports but lack access to owner, error or credential-related fields exposed by a local extension. Mapper membership first excludes the wider Python object surface; a policy for that identity, endpoint and purpose then narrows the mapped set. Structured logs and independent tests should make both the resolution decision and the authorization decision visible.
The report card from the opening scene can now finish its journey. Status resolves to a mapped Request column, Type activates the program's deliberate Operation join, comparison values remain bound data, and direction becomes one of two service-owned operations. A fitted legal card crosses the open gate into SQLAlchemy; an inherited attribute stops at mapper inspection. No desk guesses what a string might mean, and another operator can reproduce every acceptance or rejection from artifact, process and log evidence.
The final claim can therefore be exact. Official material proves a Critical authenticated code-execution path and the fixed ORM-column check. Per-instance deployment evidence can prove that a fleet runs that check. Historical telemetry may instead support three distinct findings: suspicious activity found, no indicators observed within a stated population, or exposure still unresolved. Code owners maintain the finite vocabulary, release engineers maintain artifact and process proof, operators review versions and rejection signals, and incident staff maintain retention and findings. The next ordinary card reaches the last desk with only product-owned choices; it never accidentally acquires a programming language.
Research record
9Evidence, objects, and sources
The material below preserves the identifiers and references used in this report.
9.1Research objects
Products, actors, techniques, affected objects, and control points discussed in the report.
Authenticated Python evaluation in DIRAC RequestManager web reporting
Official DIRAC advisory published on Jul 13, 2026
Official CVSS 3.1 vector AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
Maintained repair points listed by the advisory
Three RequestDB reporting paths repaired in v9.1.10
Expected fixed-build error for an unsupported or inherited model name
Mapped-column selection check introduced by the repair
9.2Event chronology
- Global advisory lists three disjoint constraints
>= 6, < 8.0.79 ; >= 8.1.0a1, < 9.0.22 ; and >= 9.1.0, < 9.1.10 are affected; the upper bounds are the corresponding first patched releases.
- Three fixed releases are available
DIRAC released 8.0.79 on Jun 1 and 9.0.22 plus 9.1.10 on Jun 4.
- DIRAC publishes GHSA-9jpv-c7p4-997x
The public record assigns Critical severity, CVSS 9.9, and authenticated server-side command execution impact.
- SOSEC completes fixed-source reconstruction
SOSEC cross-checked the official tag references and resolved commit IDs against the handler paths, eight web-query evaluation sites, replacement helpers, and regression tests.
9.3Sources and material
- DIRAC GHSA-9jpv-c7p4-997x security advisoryhttps://github.com/DIRACGrid/DIRAC/security/advisories/GHSA-9jpv-c7p4-997x
- GitHub Advisory Database entry with disjoint affected rangeshttps://github.com/advisories/GHSA-9jpv-c7p4-997x
- Official DIRAC v9.1.9 to v9.1.10 comparisonhttps://github.com/DIRACGrid/DIRAC/compare/v9.1.9...v9.1.10
- Vulnerable v9.1.9 ReqManagerHandlerhttps://github.com/DIRACGrid/DIRAC/blob/v9.1.9/src/DIRAC/RequestManagementSystem/Service/ReqManagerHandler.py
- Vulnerable v9.1.9 RequestDBhttps://github.com/DIRACGrid/DIRAC/blob/v9.1.9/src/DIRAC/RequestManagementSystem/DB/RequestDB.py
- Fixed v9.1.10 RequestDBhttps://github.com/DIRACGrid/DIRAC/blob/v9.1.10/src/DIRAC/RequestManagementSystem/DB/RequestDB.py
- Fixed v9.1.10 RequestDB regression testshttps://github.com/DIRACGrid/DIRAC/blob/v9.1.10/src/DIRAC/RequestManagementSystem/DB/test/Test_RequestDB.py
- Official DIRAC release and tag recordshttps://github.com/DIRACGrid/DIRAC/releases
- DIRAC Request Management System documentationhttps://dirac.readthedocs.io/en/latest/DeveloperGuide/Systems/RequestManagement/index.html
- DIRAC ReqManager and authorization examplehttps://dirac.readthedocs.io/en/latest/DeveloperGuide/Systems/RequestManagement/index.html#reqmanager-and-reqproxies
- SQLAlchemy mapped-attribute inspection guidancehttps://docs.sqlalchemy.org/en/20/faq/ormconfiguration.html#how-do-i-get-a-list-of-all-columns-relationships-mapped-attributes-etc-given-a-mapped-class
- SQLAlchemy runtime inspection APIhttps://docs.sqlalchemy.org/en/20/core/inspection.html
- SQLAlchemy Mapper.column_attrs APIhttps://docs.sqlalchemy.org/en/20/orm/mapping_api.html#sqlalchemy.orm.Mapper.column_attrs
- Python eval documentation and untrusted-input warninghttps://docs.python.org/3/library/functions.html#eval
- Python data model and attribute lookuphttps://docs.python.org/3/reference/datamodel.html
- Python contextual logging guidancehttps://docs.python.org/3/howto/logging-cookbook.html#adding-contextual-information-to-your-logging-output