Implement Hardened Universal Webhook Ingestion Endpoint - #8
Conversation
…security hardening and strict validation - Update POST /webhooks/:workflow_id route handler to reject missing, invalid, or unconfigured secrets with 403 Forbidden in constant-time using crypto.timingSafeEqual. - Strip shell metacharacters from trigger payload in Worker as a first layer of sanitization. - Add robust single-quote shell escaping to script executor when interpolating trigger variables into bash command nodes. - Update test cases in scripts/test-api.js to assert 403 Forbidden on bad secrets. Co-authored-by: aethelred-agent-factory <238771426+aethelred-agent-factory@users.noreply.github.com>
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
📝 WalkthroughWalkthroughThe PR removes an execution record, adds shell escaping for trigger-derived run commands, changes invalid webhook-secret responses from 401 to 403, recursively sanitizes webhook payload strings, removes ChangesCommand interpolation
Webhook hardening
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@scripts/executor.js`:
- Around line 225-233: Replace the shellEscape-based interpolation in the
command resolution flow with safe argv or environment-variable passing for all
trigger-derived values, preserving their data as values rather than shell
source. Track trigger provenance through intermediate nodes so values remain
protected when reused, and reject or otherwise prevent interpolation in shell
contexts that cannot be safely represented. Update the logic around interpolate
and resolvedConfig.command; do not rely on quote-wrapping or the immediate
producer’s nodeRes.type.
In `@worker/src/index.js`:
- Around line 150-152: Remove the character-stripping behavior from
sanitizeValue in the trigger payload handling so incoming webhook data remains
unchanged, including URLs, text, and multiline content. Preserve the raw value
through trigger-node outputs, and apply context-specific shell, URL, or header
escaping only at the corresponding rendering sink.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: d707ea4d-fc83-471f-8323-28dd5ca50c24
📒 Files selected for processing (5)
executions/.gitkeepexecutions/feature-test-1784077938478.jsonscripts/executor.jsscripts/test-api.jsworker/src/index.js
💤 Files with no reviewable changes (1)
- executions/feature-test-1784077938478.json
| const shellEscape = (val, nodeType) => { | ||
| const isTrigger = ['webhook_trigger', 'cron_trigger', 'telegram_event_trigger'].includes(nodeType); | ||
| if (isTrigger) { | ||
| // Safely wrap the string in single quotes, escaping any single quotes inside. | ||
| return "'" + String(val).replace(/'/g, "'\\''") + "'"; | ||
| } | ||
| return val; | ||
| }; | ||
| resolvedConfig.command = interpolate(node.config.command, nodeResults, shellEscape); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift
Do not interpolate untrusted values into arbitrary shell source.
This escaping is bypassable in two ways:
- If the placeholder is inside double quotes, e.g.
printf "x={{ nodes.webhook.outputs.value }}", a payload such as$(touch /tmp/pwned)becomesprintf "x='$(touch /tmp/pwned)'"; the command substitution still executes because single quotes are literal inside double quotes. - Escaping is based only on the immediate producer’s
nodeRes.type. A trigger value can be safely echoed by onerunnode and then reused from that node’s output by anotherrunnode, where this helper returns it unescaped.
Pass trigger-derived values through argv/environment variables (or use a shell-aware parser and reject unsafe interpolation contexts) instead of treating quote-wrapping as a universal shell boundary.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/executor.js` around lines 225 - 233, Replace the shellEscape-based
interpolation in the command resolution flow with safe argv or
environment-variable passing for all trigger-derived values, preserving their
data as values rather than shell source. Track trigger provenance through
intermediate nodes so values remain protected when reused, and reject or
otherwise prevent interpolation in shell contexts that cannot be safely
represented. Update the logic around interpolate and resolvedConfig.command; do
not rely on quote-wrapping or the immediate producer’s nodeRes.type.
| const sanitizeValue = (val) => { | ||
| if (typeof val === 'string') { | ||
| return val.replace(/[;&|`$><\\\r\n]/g, ''); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
sed -n '1,260p' worker/src/index.jsRepository: quickerup/telegram-workflow-bot
Length of output: 9992
🏁 Script executed:
rg -n "dispatchWorkflow|sanitizeValue|run-command|shell|escape" worker/src/index.jsRepository: quickerup/telegram-workflow-bot
Length of output: 1426
🏁 Script executed:
sed -n '260,520p' worker/src/index.jsRepository: quickerup/telegram-workflow-bot
Length of output: 9555
🏁 Script executed:
sed -n '1551,1715p' worker/src/index.jsRepository: quickerup/telegram-workflow-bot
Length of output: 5867
🏁 Script executed:
rg -n "trigger_payload|client_payload|run-workflow|dispatches|workflow_dispatch|run-command|shell command|sanitizeValue" -g '!worker/src/index.js' .Repository: quickerup/telegram-workflow-bot
Length of output: 648
🏁 Script executed:
sed -n '1,260p' scripts/executor.jsRepository: quickerup/telegram-workflow-bot
Length of output: 9579
🏁 Script executed:
rg -n "triggerPayload|trigger_payload|trigger_data" scripts/executor.jsRepository: quickerup/telegram-workflow-bot
Length of output: 442
🏁 Script executed:
sed -n '260,520p' scripts/executor.jsRepository: quickerup/telegram-workflow-bot
Length of output: 6401
Preserve the trigger payload here; escape at the sink. This strips valid webhook content before it reaches trigger-node outputs, so downstream HTTP/notify steps can see corrupted URLs, text, and multiline data. Keep the incoming payload raw and apply shell/URL/header escaping only where the value is rendered into that sink.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@worker/src/index.js` around lines 150 - 152, Remove the character-stripping
behavior from sanitizeValue in the trigger payload handling so incoming webhook
data remains unchanged, including URLs, text, and multiline content. Preserve
the raw value through trigger-node outputs, and apply context-specific shell,
URL, or header escaping only at the corresponding rendering sink.
This change implements the Universal Webhook Ingestion Endpoint at POST
/webhooks/:workflow_id.Security and Hardening features:
X-Workflow-Secretheader in constant-time usingcrypto.timingSafeEqualvia a safe comparison helper.403 Forbiddenif the secret is missing, invalid, or if the webhook has no secret configured (security-by-default).; & |$ > < \ \r \n`) from string fields in the incoming payload inside the Worker (first layer of defense).scripts/executor.js) to ensure interpolated trigger payload variables insideruncommands are parsed strictly as string literals and never as executable code or commands (second layer of defense).chat_idvalues, forcing resolution strictly from server-side configuration.All integration and security verification tests pass.
PR created automatically by Jules for task 663888368407500014 started by @aethelred-agent-factory
Summary by CodeRabbit
403 Forbiddenresponse.