KeeperHub / KeeperHub/keeperhub

`matchesRegex` compiles to `new RegExp(...).test(...)`, which the Condition validator and interpreter both reject

Open
#2,407 2 comments 0 reactions 0 assignees View on GitHub
accepted confirmed
Dominant language
TypeScript
Stars
24
Forks
93
Avg merge
1d 4h
Merged PRs (30d)
253

Description

### Before filing
- [x] searched open and closed — #1525 (the safe-eval interpreter, merged 2026-06-11) is the change that made this unreachable; nothing since mentions the operator
- [x] reproduced on `staging` 501be79 by reading the three files below; reproduced on production 2026-09-11T08:01:51Z, execution `4y5c1zst9hoegqkybyhvj`
- [x] one problem
- [x] not a security vulnerability

### Reason: reproduction
```shell
# 1. A Condition node whose expression uses the documented operator:
# {{@trigger-1:Webhook.safeAddress}} matchesRegex ^0x[0-9a-fA-F]{40}$
# 2. Run the workflow.
#
# Or without a workflow — the validator rejects the raw expression:
node -e '
const { validateConditionExpression } = require("./lib/workflow/nodes/condition/validator");
console.log(validateConditionExpression("{{@a:B.x}} matchesRegex ^0x[0-9a-fA-F]{40}$"));
console.log(validateConditionExpression("new RegExp(\"^0x\").test(String({{@a:B.x}}))"));
'
```

### Reason: what happened
Execution `4y5c1zst9hoegqkybyhvj`, node `gate-addr`, status `error`:

```
Failed to evaluate condition expression: Condition expression validation failed:
Cannot index "0x[...]". Reference step outputs with the {{@nodeId:Label.field}}
template format - use the component name for tuple/struct outputs (e.g.
{{@nodeId:Label.result.liquidityIndex}}) and bracket indexing only inside the
field path for arrays (e.g. {{@nodeId:Label.result.items[0]}}).
Original: "{{@trigger-1:Webhook.safeAddress}} matchesRegex ^0x[0-9a-fA-F]{40}$"
```

The message is about template syntax; the template syntax was fine. Three separate checks make
the operator unreachable in any spelling:

1. `lib/workflow/nodes/condition/validator.ts:91` — `BRACKET_EXPRESSION_PATTERN = /(\w+)\s*\[([^\]]+)\]/g`
runs on the raw expression with string literals unmasked, so any character class preceded by
a word character (`0x[`, `a-f[`) fails `checkBracketExpressions` even inside quotes.
2. `validator.ts:37` — `DANGEROUS_PATTERNS` contains `/\bnew\s+\w/`, so the compiled form the
builder emits (`condition/expression.ts:119-120`: `new RegExp(${right}).test(String(${left}))`)
is rejected before evaluation.
3. `lib/workflow/nodes/condition/safe-eval.ts:38-46` — `ALLOWED_METHODS` is
`includes/startsWith/endsWith/toString/toLowerCase/toUpperCase/trim`; no `test`, and the
interpreter has no `new`. So even if the validator passed it, the interpreter could not run it.

### Reason: what you expected, and what told you to expect it
`docs/workflows/creating.md:123` lists `matchesRegex | matches regex | Pattern | Left operand
matches regex pattern in right operand` among the Condition operators, and the visual builder
offers it. `tests/unit/condition-builder-utils.test.ts:202-217` asserts the builder *generates*
`new RegExp("^[a-z]+@").test(String("email"))`; nothing asserts it evaluates. It stopped
evaluating when #1525 replaced `new Function` with the allowlisted interpreter — correctly, for
the RCE reason in that PR — and the operator was never revisited.

### Reason: what it costs
An author who uses the documented operator gets a workflow that fails on its first run with an
error pointing at the wrong thing. For us it was the address gate in front of a Code node that
splices caller input as a JSON value — the one check that is supposed to reject hostile input at
the boundary. The workaround (`startsWith("0x") && .length === 42`) is weaker than the regex and
had to be discovered by reading the validator source.

### Where you saw it
Production (app.keeperhub.com)

### Version or commit
`staging` 501be79 (2026-09-11T09:08:03Z); production request 2026-09-11T08:01:51Z

### Scope: what this covers, and what it does not
Affects every Condition expression using `matchesRegex`, whether typed as an expression or built
visually — the builder path produces the banned `new RegExp` form, the expression path trips the
bracket check first. Checked `contains`, `startsWith`, `endsWith`, `.length`, `===` on the same
node: all fine. Did not check whether the `matchesRegex` *parser* in `condition-builder-utils`
(`parseFirstRule`, test at `:574-581`) is used anywhere that reads stored expressions back into
the UI — if it is, an existing stored regex condition renders in the builder and fails at
runtime, which is the same defect seen from the other side, not a separate one.

### Plan: what should happen next
Either of two; the first is smaller.

1. **Remove the operator** from `docs/workflows/creating.md`, the builder's operator list, and
`expression.ts`, with a validator error that names the replacement: *"matchesRegex is not
supported; use startsWith / endsWith / contains, or a Code node."* Changes nothing that
currently succeeds, because nothing using it currently succeeds.
2. **Implement it natively:** have `safe-eval` recognise `matchesRegex` as an operator token and
evaluate it with a pattern compiled from a string literal only (no flags, no interpolation,
length-capped, and rejected if it is not a literal), and have the validator mask string
literals before the bracket and dangerous-pattern checks. This keeps #1525's guarantee — no
`new`, no user-controlled callables — while making the documented operator real.

I would take option 2 if it is accepted; the string-literal masking is a small change with a
test, and the operator evaluation is ~30 lines in `safe-eval`.

Contributor guide

Open the contributing guide

Research direction

Start with lib/workflow/nodes/condition/validator.ts, condition/expression.ts, and safe-eval.ts, then run the validator reproduction and inspect tests/unit/condition-builder-utils.test.ts:202-217. Confirm which proposed path is accepted: either the documented operator is removed with a replacement error, or it evaluates safely without new or user-controlled callables; add coverage for the chosen behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
node.js, typescript
Domain
backend, security, testing
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.