A CI job passes an untrusted commit author name through an environment variable, then a later step builds a JSON webhook payload from it — what is still wrong?
answer
- safe for which parser?
- the shell was not the last one
- quotes close strings in JSON too
- serialize, never concatenate
- the asset is audit truth
basics
~20 sThe environment variable only protects the shell. Concatenating the name into a JSON string makes it syntax for the JSON parser: a crafted author name closes the string and adds fields, forging the deploy record that the notification leaves behind.
solid answer
~50 sThe handoff answered one question — is this value part of the script text? — and people read it as a blanket sanitization. It is not: taint is per sink. Here the shell is safe and the next parser is the victim. An author name of `x", "channel": "incident-war-room", "text": "all clear` closes the string and injects sibling keys, because the payload is assembled with string concatenation. The fix is the same as before, one layer down: never build structured documents by pasting. Serialize with something that encodes values — a JSON tool that takes the value as a named argument, or a small script in the build language — so the name lands inside a quoted string no matter what it contains. The damage here is audit truth: the deploy record and the notification are what responders read to reconstruct who shipped what, and a forged one breaks that.
code
yaml · 10 lines- name: Announce deploy
env:
AUTHOR: ${{ github.event.head_commit.author.name }} # handed off safely
run: |
curl -sS -X POST "$WEBHOOK_URL" \
-H 'content-type: application/json' \
-d "{\"text\": \"deploy by $AUTHOR\", \"channel\": \"releases\"}"
# AUTHOR = x", "channel": "nowhere", "text": "routine config change
# ...go deeper
Take away one rule: never build JSON or YAML by pasting a variable into a string. Use something that encodes the value for you, even when the value looks harmless.
Explain that safety is per parser: the environment variable settled the shell's question and says nothing about the JSON parser, so a quote in the value ends a string and starts new fields.
Trace the value through every sink in the pipeline and name the impact precisely — forged deploy records and misattributed changes are an integrity and non-repudiation loss, even when no secret moves.
Own the standard that structured documents are always serialized, never concatenated, and make sure the organisation's definition of a security incident covers corrupted audit and change records, not only data loss.
## Taint does not end at the shell The environment-variable handoff is the right fix for one specific sink: the shell that parses the step's script. Once teams learn it, a predictable failure follows — the value is treated as "cleaned" and then flows onward. But a value is never clean in the abstract. It is safe *for a given parser*, and every parser it meets afterwards is a new decision. In this job the sequence is: the engine sets `AUTHOR` in the environment (safe), the shell expands `"$AUTHOR"` (safe), and the script pastes the expansion into a JSON document (not safe). The shell hands the bytes to the next program exactly as they arrived, which is what you asked for; the JSON parser on the other end then reads the attacker's quote character as the end of a string. ## What the injected value can do An author name of `x", "channel": "incident-war-room", "text": "all clear` turns a one-field message into a document with keys the pipeline never intended. Depending on the receiver, that means: - **Rewriting the message body**, so the recorded deployment says something false. - **Overriding a routing field** — a channel, a recipient, an environment name — so the record goes somewhere nobody watches, or somewhere it will cause noise and be muted. - **Setting a field the receiver acts on.** Deployment and chat webhooks frequently accept fields that trigger behaviour: a mention that pages an on-call engineer, a link that renders, a flag that marks a deploy approved or a run successful. - **Breaking the request entirely**, which is the benign case, and the one that gets the bug found by accident. Most duplicate keys are resolved last-wins by common parsers, but you should not reason about which one wins. Assume the attacker can set any field in the document. ## Why this is worse than "a cosmetic notification" The reflex is to say a chat message is not a security boundary. The asset here is not confidentiality or availability — it is **audit truth**. The deploy notification, the release record and the change log are what people reconstruct history from during an incident and what an auditor samples to show that changes were authorised. An attacker who can write arbitrary content into that stream can: - make a real deployment look like a routine one, or attribute it to someone else, defeating non-repudiation; - insert a plausible-looking entry for a change that never happened; - push the true entry out of view among forged ones. And the attacker position is cheap: an outside contributor sets the git author name locally, on any commit, with no account privileges at all. It travels through review — reviewers read diffs, not author fields — into the default branch, and the deploy job reads it there. ## The general rule and the concrete fix **Never build a structured document by string concatenation.** Not JSON, not YAML, not a query, not a manifest, not HTML in a build report. Use a serializer that takes the value as a discrete input and encodes it: - pass the value as a named argument to a JSON-building command-line tool rather than pasting it into a heredoc; - or write a few lines in whatever language the pipeline already has, building an object and serializing it; - or, where the shape is genuinely known and narrow, validate the value against an allowlist pattern at the boundary and reject anything else. Escaping quotes yourself is the wrong answer for the same reason it was wrong at the shell layer: you now own a model of another parser's grammar, including its escape sequences, unicode handling and control characters. ## Finding the other sinks When you fix one of these, look for the rest, because the same value usually has several destinations. Common secondary sinks in build pipelines: | Sink | What the value becomes | | --- | --- | | A JSON or YAML body built by pasting | Document structure | | A generated script or config file | Code, at the moment something runs it | | A query sent to a build database | Query syntax | | An HTML build report | Markup, then script in a reviewer's browser | | A container image tag or label | An argument to later tooling, parsed downstream | | A log line ingested by a log pipeline | A forged event in the log store | | A file path or directory name | A path, with traversal available | A review heuristic that catches most of it: grep the pipeline for every place an untrusted variable appears next to a quote character or a concatenation operator, then ask what parses the result. ## In an interview The answer that earns the question is "the handoff protects the shell, not the next parser" followed by naming the specific parser. Then get the asset right: this is an integrity and non-repudiation problem in the audit trail, not a leaked secret, and saying so shows you can reason about impact when nothing confidential moves.
- How do you build that payload safely?Hand the value to a serializer as a discrete input rather than pasting it into text — a JSON-building command-line tool that takes named arguments, or a few lines in the pipeline's scripting language that construct an object and serialize it. The encoder then owns quoting and escaping for every possible byte, which is exactly the modelling work you do not want to redo by hand.
- Where else does the same value become code after it has left the shell?Anywhere something parses it: a generated script or config file, a YAML manifest built by pasting, a query, an HTML build report, a log line ingested by a log store, a directory name or file path. Each is a separate decision with a separate encoder, which is why 'sanitized' is not a property a value can carry around.
- Nothing confidential leaked and the service stayed up. Why still treat this as a real finding?Because the compromised asset is the audit trail. Deploy records and notifications are what incident responders use to reconstruct who changed what and what auditors sample to show changes were authorised. An attacker who can write arbitrary fields there can misattribute a real deployment or manufacture a plausible entry, which defeats non-repudiation even with no data loss.
- Should the receiving webhook be doing validation too?It helps, and a receiver that rejects unexpected fields or verifies a signature on the request narrows what a forged payload achieves. But you cannot rely on it: the receiver is usually a third-party integration whose parsing you do not control, and the flaw is in your document construction. Fix the sender and treat receiver-side checks as defence in depth.
Escaping for the shell and then pasting the result into JSON is like HTML-escaping a value and then dropping it into a SQL statement — you defended the wrong grammar.
saying these in an interview costs you the question
- Believes the environment variable sanitized the value everywhere
- Dismisses a notification as cosmetic and therefore harmless
- Escapes double quotes by hand instead of serializing
- Assumes the receiver will reject a malformed payload
- Treats the merge as evidence the author name was reviewed