Unauthenticated remote code execution via `eval()` on format-string
The otterdog webapp evaluates a format-string built from the incoming GitHub webhook JSON body using Python's eval() . When the GITHUB_WEBHOOK_SECRET configuration value is unset or empty (the code default is None ), the HMAC signature check is skipped entirely, allowing an unauthenticated network client to reach the eval() sink with attacker-controlled input and execute arbitrary Python on the webapp host.
An unauthenticated remote attacker who can send HTTP requests to the webapp's webhook endpoint (default POST /github-webhook/receive , bound on 0.0.0.0:5000 in the shipped container configuration) can execute arbitrary code as the webapp process. The attack requires that the deployment has not set GITHUB_WEBHOOK_SECRET ; the documented deployment procedure ( TESTING.md ) instructs setting it, but the application starts without warning when it is absent and the signature check silently becomes a no-op. With a non-empty secret correctly configured, only GitHub can produce a valid signature and GitHub's action field is drawn from a fixed enum of safe values, so the injection is not reachable.
All versions of the otterdog webapp (source checkout or ghcr.io/eclipse-csi/otterdog container deployment) up to and including current main .
The otterdog package on PyPI is not affected : otterdog/webapp is explicitly excluded from the published wheel ( pyproject.toml exclude = ["otterdog/app.py", "otterdog/webapp"] ).
Not yet patched. No fix commit is present on the default branch.
The sink at github_webhook.py:160 builds an expression by substituting the request body's action field into a single-quoted template and passes the result to eval() :
End-to-end against a deployment with no GITHUB_WEBHOOK_SECRET configured:
_log_event is called before pydantic validation of the event model, so the body need not be a fully valid WorkflowJobEvent .
Replace the eval(EVENT_FILTER[event_type].format(**data)) construction at github_webhook.py:160 with a plain dict-of-callables lookup, e.g. EVENT_FILTER = {"workflow_job": lambda d: d.get("action") == "queued", ...} . Separately, fail closed at startup when GITHUB_WEBHOOK_SECRET is None or empty so that signature verification cannot be silently skipped.
— the vulnerable EVENT_FILTER / eval() sink
— CWE-95: Improper Neutralization of Directives in Dynamically Evaluated Code ('Eval Injection')
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
