ci(security): hold API responses in a file instead of piping them onward
Security Scan (reusable) / Security Scan (push) Successful in 25s
Security / scan (push) Successful in 25s
Build / Build (Release) (push) Successful in 28s

Semgrep blocks on gha-curl-pipe-shell, and it is pointing at the publish step
added an hour ago. The rule exists for `curl https://…/install.sh | bash`:
remote content reaching an interpreter. What this actually did was pipe a JSON
response into a python3 -c inline script -- the interpreter and its program both
live in the workflow file, and the server only ever supplied data.

A false positive, then, but the rule cannot see the difference between an
interpreter reading a program from stdin and one reading data, and neither can
the next person to read the step. Responses go to a file and are read from
there. Same shape as the finding suggests, and worth having anyway: a response
that is on disk can be looked at when a call misbehaves, instead of vanishing
into a pipe.

Verified by falsification rather than by a green run: the same ruleset against
the previous revision of this file reports 1 blocking finding, against this one
zero. Without that check a passing scan only proves the rule was not loaded.
This commit is contained in:
2026-08-19 22:49:07 +02:00
parent a4c4e15c3b
commit 38c6707970
+31 -16
View File
@@ -174,28 +174,43 @@ jobs:
api="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}" api="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}"
auth="Authorization: token ${GITEA_TOKEN}" auth="Authorization: token ${GITEA_TOKEN}"
# Existing release for this tag, or create one. # Responses land in a file before anything reads them, rather than
rel_id="$(curl -sf -H "$auth" "$api/releases/tags/${TAG_NAME}" \ # being piped straight into an interpreter. The interpreter is inline
| python3 -c 'import sys,json; print(json.load(sys.stdin).get("id",""))' 2>/dev/null || true)" # either way and the server only ever supplies data, but a scanner
# cannot tell those apart from curl-pipe-shell -- and holding the
# response makes it inspectable when a call misbehaves.
get_release_id() {
if curl -sf -H "$auth" "$api/releases/tags/${TAG_NAME}" -o release.json; then
python3 -c 'import json; print(json.load(open("release.json")).get("id",""))'
fi
}
rel_id="$(get_release_id || true)"
if [ -z "$rel_id" ]; then if [ -z "$rel_id" ]; then
payload="$(python3 -c ' python3 - <<'PYCREATE' > create.json
import json, os, sys import json, os
body = open("release-body.md", encoding="utf-8").read() body = open("release-body.md", encoding="utf-8").read()
json.dump({"tag_name": os.environ["TAG_NAME"], "name": os.environ["TAG_NAME"], print(json.dumps({
"body": body, "draft": False, "prerelease": False}, sys.stdout) "tag_name": os.environ["TAG_NAME"],
')" "name": os.environ["TAG_NAME"],
rel_id="$(printf '%s' "$payload" \ "body": body,
| curl -sf -X POST -H "$auth" -H "Content-Type: application/json" -d @- "$api/releases" \ "draft": False,
| python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')" "prerelease": False,
}))
PYCREATE
curl -sf -X POST -H "$auth" -H "Content-Type: application/json" \
-d @create.json "$api/releases" -o created.json
rel_id="$(python3 -c 'import json; print(json.load(open("created.json"))["id"])')"
echo "Created release $rel_id for ${TAG_NAME}" echo "Created release $rel_id for ${TAG_NAME}"
else else
echo "Reusing release $rel_id for ${TAG_NAME}" echo "Reusing release $rel_id for ${TAG_NAME}"
fi fi
# Drop a same-named asset from an earlier attempt, or the upload 409s. # A same-named asset from an earlier attempt has to go, or the upload
old_id="$(curl -sf -H "$auth" "$api/releases/${rel_id}/assets" \ # collides with it. This is the state a recovery run finds.
| python3 -c 'import sys,json; print(next((a["id"] for a in json.load(sys.stdin) if a["name"]=="latest.zip"), ""))' 2>/dev/null || true)" curl -sf -H "$auth" "$api/releases/${rel_id}/assets" -o assets.json
old_id="$(python3 -c 'import json; print(next((a["id"] for a in json.load(open("assets.json")) if a["name"]=="latest.zip"), ""))')"
if [ -n "$old_id" ]; then if [ -n "$old_id" ]; then
curl -sf -X DELETE -H "$auth" "$api/releases/${rel_id}/assets/${old_id}" curl -sf -X DELETE -H "$auth" "$api/releases/${rel_id}/assets/${old_id}"
echo "Replaced existing latest.zip (asset $old_id)" echo "Replaced existing latest.zip (asset $old_id)"
@@ -203,5 +218,5 @@ jobs:
curl -sf -X POST -H "$auth" \ curl -sf -X POST -H "$auth" \
-F "attachment=@${ZIP_PATH};filename=latest.zip" \ -F "attachment=@${ZIP_PATH};filename=latest.zip" \
"$api/releases/${rel_id}/assets?name=latest.zip" \ "$api/releases/${rel_id}/assets?name=latest.zip" -o uploaded.json
| python3 -c 'import sys,json; a=json.load(sys.stdin); print("Attached", a["name"], a["size"], "bytes")' python3 -c 'import json; a=json.load(open("uploaded.json")); print("Attached", a["name"], a["size"], "bytes")'