Guide 109 decision guide

How to separate a dangerous API call from an exploitable injection path

Separate a dangerous API from an exploitable injection path with an explicit evidence ledger, authority boundaries, constrained language rules, and counterevidence.

A call to eval, a shell, a query executor or an HTML interpreter deserves attention, but the call itself is not a complete injection finding. To decide whether it is a supported vulnerability, establish who controls the data, how it reaches the call, what validation or separation occurs, whether the path is used in the assessed deployment, and what harmful action could follow. A fixed expression removes direct caller control of that expression's syntax, but the exposed objects, functions and execution authority still matter. A request-controlled expression reaching that API adds a direct injection concern. The answer can also be uncertain if the deployed caller, policy or runtime behavior is unknown. Do not turn that uncertainty into “confirmed” or “safe.”

The distinction matters because developers need to know what to change, and security leads need to know what is actually supported by evidence. CWE-94 defines code injection around externally influenced input changing generated code. OWASP's secure code review guidance recommends following input through transformations to sinks and across trust boundaries. A flagged sink gives you a starting location, not the rest of the path.

The saved formula is a lifecycle, not just an argument

Synthetic application and code; no expressions were executed. Wren Metrics has two callers of a helper that invokes Python eval: an application-owned literal scoring rule and a scheduled report that loads a tenant's saved formula. The literal caller supplies no externally editable syntax. That narrows its injection allegation, while replacing an unnecessary evaluator may still be sensible cleanup.

The scheduled path is different:

rule editor → draft row → publish action → scheduled job → worker → eval

A tenant rule editor can save 2 * total as draft F9/v3. An administrator publishes that version. The queue contains rule_id=F9, and the worker later loads the row's current expression. Python's documentation warns that eval executes arbitrary code; naming a namespace safe_globals does not establish an isolation boundary. Tenant administration is also not permission to run code with the service's authority.

Start with the exact stored records:

draft:       F9/v4, tenant Cedar, expression changed after approval
publication: F9/v3, approved by account administrator
job E7:      rule_id F9, tenant Cedar, no version
worker:      load current draft for F9 → evaluate expression

The worker is not evaluating what was approved. Even a genuinely constrained formula language would have a version-binding defect here. With unrestricted Python evaluation, the separate syntax boundary also needs repair. Do not merge these into a vague claim that “the database is untrusted.” Identify which writer can change which version and which reader consumes it.

Decide how much language the feature actually needs

Wren offers only two documented operations. A small operation record can meet that requirement without evaluating customer text:

{"operation":"cap","limit":100,"input_field":"total"}

An illustrative evaluator handles a deliberately narrow domain:

def apply_rule(rule, value):
    if type(value) is not int or not 0 <= value <= 1_000_000:
        raise ValueError("invalid value")
    if type(rule) is not dict:
        raise ValueError("invalid rule")
    if rule == {"operation": "double", "input_field": "total"}:
        return value * 2
    if (set(rule) == {"operation", "limit", "input_field"}
            and rule["operation"] == "cap"
            and rule["input_field"] == "total"
            and type(rule["limit"]) is int
            and 0 <= rule["limit"] <= 1_000_000):
        return min(value, rule["limit"])
    raise ValueError("unsupported rule")

The bounds and operation set are example product choices, not universal limits. Given the shown cap record and total=140, the expected result is 100; a request for a new operation is rejected. Values such as True are rejected by the exact integer type check rather than being silently treated as numbers. The snippet is unexecuted and omits storage, authorization and output validation. Pass ordinary decoded data of the accepted shape, not arbitrary plugin objects: exact checks at this boundary do not qualify custom objects or their overloaded behavior elsewhere.

If customers need expressions such as arithmetic over several fields, do not pretend that two operations satisfy them. Define a deliberately limited language: allowed operators and names, numeric bounds, expression size/depth, evaluation work, errors and output type. Choose and review an implementation against that specification. Prohibiting a few words around Python eval is not that implementation. Even ast.literal_eval is a literal parser, not a replacement arithmetic language; inspect its documented limits before selecting it for another job.

Bind execution to approved data and current authority

The repaired job names F9/v3, its tenant and the approved operation-schema version. The worker loads that immutable approved record, verifies it belongs to Cedar, and checks whether the job's current delegation permits this report. If the version was revoked, it follows the explicit cancellation policy rather than falling back to the newest draft. Replaying E7 must not silently activate v4.

The evaluator receives a small authorized projection such as {total: 140}, not a live database object or helpers that can retrieve other accounts. Syntax restriction and data authority are separate. A harmless arithmetic language can still reveal another customer's data if the service supplies the wrong inputs.

The useful completion record now distinguishes four cases:

Case Decision
Application literal caller No externally controlled expression shown; narrow the original alert
Tenant-edited text reaches Python eval Source-supported code-evaluation concern; deployment and authority define practical effect
Approved v3 replaced by draft v4 at execution Separate approval/version defect, even after language restriction
Fixed operation record, pinned approved version, scoped data Intended replacement design; requires implementation and deployment validation

Wren's alert should become a small chain of propositions. A tenant rule editor can create a formula. A publication action makes a particular version eligible for scheduled work. A job selects a version. The worker passes that version's expression to an evaluator with a particular data context and process authority. Each proposition needs evidence; the highlighted eval line establishes only the final interpretation step.

Use a ledger to prevent the source trace from skipping a boundary:

Proposition Evidence needed in Wren Why it matters
Tenant actor can alter the expression Rule editor's write path and current permission Establishes external influence
Altered value reaches scheduled work Publication and queue construction Establishes the relevant lifecycle
Worker selects that value Stored version lookup and fallback behavior Distinguishes approved source from current draft
Evaluator interprets it as Python Actual helper and runtime API Establishes the language boundary
Service authority permits a harmful consequence Exposed data, helpers, process and deployment context Bounds the effect rather than assuming it

A missing row does not make the dangerous call harmless. It tells the reviewer what can be concluded now and which artifact would change that conclusion. A complete source defect may justify replacing an unnecessary evaluator even while deployment mapping is pending. The report should separate that engineering decision from a claim of observed exploitation.

Distinguish accidental interpretation from intentional authoring

Wren's product owner may initially say that administrators are trusted to write formulas. Ask trusted for what. A customer administrator may be authorized to define a billing calculation for their account while remaining untrusted with respect to the service's operating-system authority or another customer's records. The product's formula permission does not automatically authorize general-purpose code execution.

If the feature needs only double and cap, the operation record is proportionate. It expresses the business choice without granting syntax for an unrelated language. If customers need arithmetic expressions, document that requirement before selecting the replacement. A nominally safer parser that cannot support the needed operations may lead developers to retain a hidden fallback to eval, preserving the original risk.

The approved language should have explicit semantics for names, operators, numeric limits, errors, and output. The data supplied to it should be an authorized projection rather than live service objects. Resource limits matter because even a constrained expression can consume work; they are separate from whether it can select arbitrary Python operations. This article defines the questions but does not qualify a particular expression-engine implementation.

Walk the stale-approval case through the replacement

Replacing eval does not fix the job's missing version. Suppose v3 is the approved cap operation with limit100, while draft v4 changes the limit to a materially different business value. If E7 still loads the latest draft, the worker applies an unapproved rule even though the evaluator now accepts only two operations. The injection repair succeeded at one boundary while the approval defect remains.

Bind the job to the immutable approved operation record and its schema version. The worker should verify the record's account, approval state, and applicable delegation before use. If the approved version was revoked, the response follows the product's cancellation or reapproval rule. It must not quietly substitute the newest draft as a convenience fallback.

Also define a legitimate update. An administrator can publish v4 through the approved workflow and create future jobs that name it. Existing jobs may stay pinned to v3 or be explicitly rescheduled under policy. The important property is that changing the draft does not silently rewrite work already admitted against another approval.

For the synthetic cap example, the expected result remains100 for the approved v3 input140. That arithmetic illustrates the selected operation only. It does not demonstrate storage authorization, concurrency behavior, resource confinement, or deployed execution. Keep those evidence levels distinct when writing the repair acceptance record.

Reject the original claim without losing a useful cleanup

The application-owned literal caller deserves its own disposition. If the expression and exposed inputs are fixed by application code, the reviewer has not established an externally controlled expression through that caller. Reject or narrow that particular injection allegation with the exact caller evidence. Do not label it exploitable simply because it shares the helper with the tenant-edited path.

The developer may still remove the evaluator there for clarity and consistency. Describe that as a justified simplification, not proof that a second vulnerability existed. This distinction makes the finding count honest and helps the team understand which change closes the security path and which reduces future maintenance risk.

Finally, inspect all helper callers at the repaired revision. If the tenant path moved to a new worker, its disappearance from the original file is not closure. If a compatibility branch still accepts saved Python expressions, document how those legacy records are migrated or withheld. A clear migration plan is often the decisive part of replacing an interpreter in a real product: the new safe path must not coexist indefinitely with an undocumented old one.

No result here claims observed code execution or cross-tenant access. A reviewer can establish routing with instrumentation before evaluation in an authorized isolated environment; they need not execute untrusted expressions to identify the unsafe boundary. Finish by inspecting every writer of the formula and every worker caller at the new revision. The general interpreter guide explains the broader model; the representation trace helps when the saved and executed forms differ.