Skip to content

Implement Hardened Universal Webhook Ingestion Endpoint - #8

Merged
quickerup merged 1 commit into
mainfrom
jules-663888368407500014-abe2822b
Jul 15, 2026
Merged

Implement Hardened Universal Webhook Ingestion Endpoint#8
quickerup merged 1 commit into
mainfrom
jules-663888368407500014-abe2822b

Conversation

@quickerup

@quickerup quickerup commented Jul 15, 2026

Copy link
Copy Markdown
Owner

This change implements the Universal Webhook Ingestion Endpoint at POST /webhooks/:workflow_id.

Security and Hardening features:

  1. Validates the X-Workflow-Secret header in constant-time using crypto.timingSafeEqual via a safe comparison helper.
  2. Rejects requests with an immediate 403 Forbidden if the secret is missing, invalid, or if the webhook has no secret configured (security-by-default).
  3. Strips key shell metacharacters (like ; & | $ > < \ \r \n`) from string fields in the incoming payload inside the Worker (first layer of defense).
  4. Implements strict shell single-quote escaping in the GitHub Actions script executor (scripts/executor.js) to ensure interpolated trigger payload variables inside run commands are parsed strictly as string literals and never as executable code or commands (second layer of defense).
  5. Excludes client-supplied chat_id values, forcing resolution strictly from server-side configuration.
  6. Rate-limits requests dynamically using WORKFLOW_STATE KV store per workflow ID.
  7. Successfully dispatches the sanitized, authorized payload to GitHub Actions as a repository dispatch and returns 200 OK immediately with an execution ID.

All integration and security verification tests pass.


PR created automatically by Jules for task 663888368407500014 started by @aethelred-agent-factory

Summary by CodeRabbit

  • Bug Fixes
    • Improved webhook security by sanitizing incoming payload content before workflow processing.
    • Invalid or missing webhook secrets now return a 403 Forbidden response.
    • Added safer handling for command inputs triggered by webhooks and scheduled events.
  • Chores
    • Removed an outdated feature-test execution record.

…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>
@google-labs-jules

Copy link
Copy Markdown
Contributor

👋 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 @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The 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 chat_id, and updates integration tests.

Changes

Command interpolation

Layer / File(s) Summary
Run command shell escaping
scripts/executor.js
Run-node command interpolation now shell-escapes trigger-sourced values while other configuration fields retain standard interpolation.

Webhook hardening

Layer / File(s) Summary
Webhook authorization and payload sanitization
worker/src/index.js, scripts/test-api.js
Invalid or missing webhook secrets now expect HTTP 403, and webhook payloads remove chat_id and recursively sanitize string values before dispatch.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: google-labs-jules[bot]

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the main change: a hardened universal webhook ingestion endpoint.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jules-663888368407500014-abe2822b

Comment @coderabbitai help to get the list of available commands.

@quickerup
quickerup merged commit 8638e58 into main Jul 15, 2026
1 check was pending
@quickerup
quickerup deleted the jules-663888368407500014-abe2822b branch July 15, 2026 03:33

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 41ec7c7 and 56aabb6.

📒 Files selected for processing (5)
  • executions/.gitkeep
  • executions/feature-test-1784077938478.json
  • scripts/executor.js
  • scripts/test-api.js
  • worker/src/index.js
💤 Files with no reviewable changes (1)
  • executions/feature-test-1784077938478.json

Comment thread scripts/executor.js
Comment on lines +225 to +233
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 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) becomes printf "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 one run node and then reused from that node’s output by another run node, 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.

Comment thread worker/src/index.js
Comment on lines +150 to +152
const sanitizeValue = (val) => {
if (typeof val === 'string') {
return val.replace(/[;&|`$><\\\r\n]/g, '');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

sed -n '1,260p' worker/src/index.js

Repository: quickerup/telegram-workflow-bot

Length of output: 9992


🏁 Script executed:

rg -n "dispatchWorkflow|sanitizeValue|run-command|shell|escape" worker/src/index.js

Repository: quickerup/telegram-workflow-bot

Length of output: 1426


🏁 Script executed:

sed -n '260,520p' worker/src/index.js

Repository: quickerup/telegram-workflow-bot

Length of output: 9555


🏁 Script executed:

sed -n '1551,1715p' worker/src/index.js

Repository: 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.js

Repository: quickerup/telegram-workflow-bot

Length of output: 9579


🏁 Script executed:

rg -n "triggerPayload|trigger_payload|trigger_data" scripts/executor.js

Repository: quickerup/telegram-workflow-bot

Length of output: 442


🏁 Script executed:

sed -n '260,520p' scripts/executor.js

Repository: 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.

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