ci(security): hold API responses in a file instead of piping them onward
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:
@@ -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")'
|
||||||
|
|||||||
Reference in New Issue
Block a user