Skip to content

fix(condition): stop a secret value from breaking or forging a condition - #6705

Merged
icecrasher321 merged 1 commit into
stagingfrom
fix/condition-secret-placeholders
Aug 14, 2026
Merged

fix(condition): stop a secret value from breaking or forging a condition#6705
icecrasher321 merged 1 commit into
stagingfrom
fix/condition-secret-placeholders

Conversation

@icecrasher321

Copy link
Copy Markdown
Collaborator

Problem

Condition expressions pasted every environment variable value into the expression as source. Block references in the same expression go through a proper escape (\, ', \n, \r, U+2028/9) and get quoted — env vars went through neither.

Verified against the real resolver:

Expression Resolved to Result
<producer.result> === 'hello world' 'hello world' === 'hello world' ✅ true
{{NAME}} === 'hello world' hello world === 'hello world' SyntaxError
'{{NAME}}' === 'bob', NAME=O'Brien 'O'Brien' === 'bob' SyntaxError
'{{NAME}}' === 'bob', NAME=x' || true || ' 'x' || true || '' === 'bob' true — forged
{{N}} === 123, N='123' 123 === 123 ✅ true

Three defects: a bare string placeholder is a SyntaxError (so the form the Function block docs recommend can't be used here at all); an apostrophe or newline in a legitimate value fails the run; and the quoted form lets a secret value forge a true branch out of a comparison that should be false.

Fix

Inline only structurally inert literals — numbers, booleans, null, with optional space/tab padding. Every other value keeps its {{NAME}} placeholder and is bound as a string by compileCodePlaceholders, the same execution-boundary compiler Function blocks and Custom Tools already use.

No change to the shared compiler, no new flag through the tool/route/contract. The whole implementation is one predicate plus a two-line call-site change in the condition-only resolution path (resolveTemplateWithoutConditionFormatting, which has exactly one caller).

Every character the predicate admits is inert in both places a placeholder can land: in expression position none introduces an operator or comment, and inside a string literal none terminates it. Line terminators are excluded — a raw newline would break a single-quoted string.

Nothing breaks

Full apps/sim suite: 25,076 passed, 0 failures. Type-check and Biome clean, check:api-validation passes.

The legacy outcomes are preserved, and there's a new test that proves it end to end — resolver → compiler → the same Boolean(...) wrapper condition-handler.ts builds:

  • {{COUNT}} === 3 → still true (inlined literal, unchanged)
  • {{ENABLED}} === true → still true (inlined literal, unchanged)
  • "Bearer {{API_KEY}}" === "Bearer token" → still true, now via compiled concatenation instead of a pasted value
  • null, negative, and exponent values all still compare as literals

Padding is admitted rather than trimmed so the inlined text stays byte-identical to the stored value — whitespace is meaningless bare but significant inside quotes, and only the untrimmed value is correct in both. Env var values aren't trimmed on save, so this is reachable; covered by a test.

The 4 new tests were confirmed to fail against the old behavior and pass against the new. The end-to-end outcomes test passes under both, which is the point — it's the regression guard.

The one deliberate behavior change

A value whose text is itself a quoted JS literal — a secret stored as 'foo', a plausible workaround someone may have found for the bare-string SyntaxError — now compares as the 5-character string rather than as source. That form is precisely the injectable one, so it can't be kept. Worth a note in release comms.

Also: the placeholder type contract in docs

A customer hit a related silent break after the resolver lift in #6247 moved Function blocks from source inlining to value binding. A workspace secret holding a JS array of regex literals started arriving as a 411-character string, their guardrail threw, and requests fell through to a fallback billed on GPT output tokens.

That change is correct and stays — it closed arbitrary code execution from a secret value. But the docs described the mechanism ("bound separately from the source") and never the consequence, while function.mdx actively recommends the bare form whose meaning changed. Added the contract and the conversions:

  • {{KEY}} in Function/Custom Tool code always evaluates to a string
  • a bare if ({{FLAG}}) is always true, because "false" is truthy
  • a list must be stored as JSON to parse back into an array

Every docs snippet was run through the real compiler before being written down.

Test plan

  • bun run test (full apps/sim) — 25,076 passed
  • bun run type-check
  • bunx biome check on changed files
  • bun run check:api-validation
  • New tests verified to fail against pre-fix behavior

🤖 Generated with Claude Code

Condition expressions pasted every environment variable value into the
expression as source. Block references in the same expression go through a
proper escape and get quoted; env vars went through neither. That left three
defects:

- A bare string placeholder was a SyntaxError. `{{NAME}} === 'alice'` resolved
  to `alice === 'alice'`, so the form the Function block docs recommend could
  not be used here at all.
- Ordinary data broke the block. An apostrophe (`O'Brien`) or a newline in a
  legitimate value produced unparseable source and failed the run.
- The quoted form was injectable. A value of `x' || true || '` turned
  `'{{NAME}}' === 'bob'` into `'x' || true || '' === 'bob'`, forging a true
  branch out of a comparison that should be false.

Inline only structurally inert literals — numbers, booleans, and null, with
optional space/tab padding. Every other value keeps its `{{NAME}}` placeholder
and is bound as a string by the execution-boundary compiler, the same one
Function blocks and Custom Tools already use.

Legacy outcomes are preserved. `{{COUNT}} === 3` and `{{ENABLED}} === true`
still compare as literals, and an embedded `"Bearer {{API_KEY}}"` still
compares equal — now via compiled concatenation rather than a pasted value.
Padding is admitted rather than trimmed so the inlined text stays
byte-identical to the stored value, which is what keeps a padded number
correct both bare and quoted.

A resolved secret also no longer travels to the execution boundary inside the
condition source.

The one deliberate behavior change: a value whose text is itself a quoted JS
literal (a secret stored as `'foo'`, a plausible workaround for the bare-string
SyntaxError) now compares as the 5-character string rather than as source.
That form is the injectable one, so it cannot be kept.

Docs: state the placeholder type contract, which was described mechanically but
never in terms of what a reader gets. `{{KEY}}` in Function and Custom Tool code
always evaluates to a string, so a bare `if ({{FLAG}})` is always true and a list
has to be stored as JSON. This is what a customer hit after the resolver lift in
 #6247 moved Function blocks off source inlining.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview Aug 14, 2026 8:29pm

Request Review

@cursor

cursor Bot commented Aug 14, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Changes how environment secrets are woven into user-authored JavaScript condition expressions—a security-sensitive path that previously allowed injection and forged comparisons. Legacy literal comparisons are preserved but quoted-literal secret workarounds now compare as strings.

Overview
Condition blocks no longer paste every {{KEY}} value into the expression as JavaScript source. Only structurally inert literals (numbers, booleans, null, with optional space/tab padding) are inlined so comparisons like {{MAX_RETRIES}} === 3 still work. Other values keep the placeholder and are bound as strings by the same compileCodePlaceholders path Function blocks use, which blocks syntax errors, apostrophe/newline breakage, and forged true branches from malicious secret text.

Docs now spell out that {{KEY}} in Function/Custom Tool code is always a string (with conversion examples) and that Condition blocks still treat numeric/boolean/null env values as literals.

Tests add resolver → compiler → Boolean(...) coverage for legacy outcomes, injection cases, bare-string comparisons, and secrets staying out of boundary code.

Reviewed by Cursor Bugbot for commit d2fbd20. Configure here.

@greptile-apps

greptile-apps Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR prevents Condition environment-variable values from being interpreted as executable source while preserving existing literal comparisons.

  • Restricts direct substitution to JavaScript numbers, booleans, and null.
  • Defers all other Condition placeholders to the shared execution-boundary compiler for string binding.
  • Adds end-to-end regression coverage for injection, quoting, newlines, literals, and padded values.
  • Documents placeholder typing and conversion behavior for Condition, Function, and Custom Tool blocks.

Confidence Score: 5/5

The PR appears safe to merge, with no concrete changed-code failure remaining after review.

The resolver now limits raw source insertion to valid inert JavaScript literals while the existing execution-boundary compiler safely binds all other environment values as strings, and the tests cover the reachable compatibility and injection cases.

Important Files Changed

Filename Overview
apps/sim/executor/variables/resolver.ts Restricts Condition source substitution to inert literals and preserves other placeholders for safe execution-boundary binding; no actionable defect was identified.
apps/sim/executor/variables/resolver.test.ts Adds resolver-to-compiler regression coverage for injection prevention, string handling, literal compatibility, and whitespace preservation.
apps/docs/content/docs/en/workflows/blocks/condition.mdx Documents the distinct literal-versus-string behavior of environment placeholders in Conditions.
apps/docs/content/docs/en/workflows/blocks/function.mdx Clarifies that Function and Custom Tool placeholders are strings and documents explicit conversions.
apps/docs/content/docs/en/workflows/variables.mdx Adds a concise cross-reference describing the string-valued placeholder contract in executable code.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A["Condition expression with {{KEY}}"] --> B["VariableResolver"]
  B --> C{"Value is number, boolean, or null?"}
  C -->|Yes| D["Inline inert literal"]
  C -->|No| E["Preserve placeholder"]
  D --> F["Condition handler builds Boolean(expression)"]
  E --> G["compileCodePlaceholders binds string value"]
  G --> F
  F --> H["Execute condition and select branch"]
Loading

Reviews (1): Last reviewed commit: "fix(condition): stop a secret value from..." | Re-trigger Greptile

@icecrasher321
icecrasher321 merged commit 237f973 into staging Aug 14, 2026
30 checks passed
@waleedlatif1
waleedlatif1 deleted the fix/condition-secret-placeholders branch August 14, 2026 21:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant