fix(ci): prevent shell injection via milestone title in code freeze workflows#5067
fix(ci): prevent shell injection via milestone title in code freeze workflows#5067kakkoyun wants to merge 1 commit into
Conversation
…orkflows
The code freeze workflows interpolated the milestone title expression
directly inside their run: shell blocks. GitHub Actions substitutes
${{ }} expressions before handing the script to bash, so a milestone
title like `Code Freeze"; <payload> #` would execute on the runner. Any
user with the Triage role can create such a milestone. Because the job
runs after dd-octo-sts mints a privileged token (id-token: write), this
is an insider privilege-escalation path that bypasses branch protection.
The startsWith() guard only checks the prefix, so the rest of the title
stays attacker-controlled.
Route the untrusted values through env: and reference shell variables in
run:, so bash expands them at runtime as data instead of parsing them as
script. This matches the indirection already used for PR titles.
Introduced by #4276. Fixes APMSP-3535.
|
@codex review for security regressions |
|
Codex Review: Didn't find any major issues. Breezy! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
🎉 All green!🧪 All tests passed 🎯 Code Coverage (details) 🔗 Commit SHA: 7a89ed0 | Docs | Datadog PR Page | Give us feedback! |
BenchmarksBenchmark execution time: 2026-07-24 11:40:49 Comparing candidate commit 7a89ed0 in PR branch Found 0 performance improvements and 0 performance regressions! Performance is the same for 326 metrics, 0 unstable metrics, 1 flaky benchmarks without significant changes.
|
What
The code freeze workflows passed the milestone title straight into a
run:shell block via${{ github.event.milestone.title }}(and, on start, theworkflow_dispatchtitle input). GitHub Actions expands${{ }}before bash sees the script, so a crafted milestone title runs as shell on the runner.This routes those values through
env:and reads them back as shell variables, so bash treats them as data. It is the same indirection already used for PR titles inpull-request-title-validation.yml.Why it matters
Creating a milestone needs only the Triage role. The freeze job runs with
id-token: writeafterdd-octo-stsmints a privileged token, so an injected command would run with that token in scope and could forge thecode-freezestatus check to bypass the merge gate. Low severity: insider-only, no external-attacker path. Still a privilege boundary worth closing.Tracking: APMSP-3535. Introduced by #4276.
Test
make lint/action(actionlint) passes on the full workflow set.${{ github.event.* }}remains inside arun:block in either file.github.event_name/github.repositoryreferences left in the unrelatedcheck_remainingstep are trusted GitHub contexts (fixed event enum /owner/reposlug), not attacker input.${{ }}in theif:guards is evaluated by the Actions expression engine, not bash, so it is not an injection vector.