ci(security): no ${{ }} in run:, validate resolved versions, LiteSpeed index over HTTPS

Security review of #30: step outputs derived from the LiteSpeed apt index
(fetched over plain HTTP) were spliced into shell via ${{ }} in jobs that
hold registry push secrets — a MITM'd "version" would be shell injection.

- every run: block in build-push.yaml and lsphp-source-emergency.yaml now
  receives matrix values, step outputs, inputs and run_id via env: only
  (incl. the pre-existing php.net / LiteSpeed resolve steps from #24/#29)
- resolved versions are regex-checked before GITHUB_OUTPUT: PHP
  ^[0-9]+\.[0-9]+\.[0-9]+$, deb ^[0-9][0-9A-Za-z.+~:-]*$
- LiteSpeed Packages index fetched over HTTPS (--proto =https); measured
  rpms.litespeedtech.com serves it (HTTP/2 200)
- lsphp-src-build.sh / lsphp-src-overlay.sh validate PHPVER too

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shadowdaoandClaude Opus 5.5 committed 2026-10-21 09:47:44 -07:00
1 parent 83c2ee9c2a
commit 42993f42bd
4 files changed
+82 -32

No files matched your search

+58 -20
View File
@@ -97,19 +97,26 @@ jobs:
# the package-upgrade layers consume CACHE_BUST and can never be reused.
- name: Cache-bust value
id: bust
run: echo "value=$(date -u +%Y%m%dT%H%M%SZ)-${{ gitea.run_id }}" >> "$GITHUB_OUTPUT"
env:
RUN_ID: ${{ gitea.run_id }}
run: echo "value=$(date -u +%Y%m%dT%H%M%SZ)-${RUN_ID}" >> "$GITHUB_OUTPUT"
# The minimum PHP version this build must ship: php.net's current release for
# the branch (over HTTPS). The Dockerfile fails if Remi gave us anything older,
# so lagging mirrors or replayed repo metadata can't ship a vulnerable PHP.
- name: Resolve current PHP release from php.net
id: phpmin
# Values reach the script via env, never via ${{ }} inside run: (a
# crafted string would otherwise be shell); outputs are regex-checked.
env:
PHPVER: ${{ matrix.phpver }}
run: |
set -euo pipefail
v="${{ matrix.phpver }}"; branch="${v:0:1}.${v:1}"
v="$PHPVER"; branch="${v:0:1}.${v:1}"
json=$(curl -fsS --retry 5 --max-time 60 "https://www.php.net/releases/index.php?json&max=1&version=${branch}")
min=$(printf '%s' "$json" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(next(iter(d)) if isinstance(d,dict) and d else "")')
if [ -z "$min" ]; then echo "::error::no php.net release found for ${branch}"; exit 1; fi
[[ "$min" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::php.net returned a malformed version for ${branch}"; exit 1; }
echo "php.net current ${branch}: ${min}"
echo "min=${min}" >> "$GITHUB_OUTPUT"
@@ -157,19 +164,26 @@ jobs:
# the package-upgrade layers consume CACHE_BUST and can never be reused.
- name: Cache-bust value
id: bust
run: echo "value=$(date -u +%Y%m%dT%H%M%SZ)-${{ gitea.run_id }}" >> "$GITHUB_OUTPUT"
env:
RUN_ID: ${{ gitea.run_id }}
run: echo "value=$(date -u +%Y%m%dT%H%M%SZ)-${RUN_ID}" >> "$GITHUB_OUTPUT"
# The minimum PHP version this build must ship: php.net's current release for
# the branch (over HTTPS). The Dockerfile fails if Remi gave us anything older,
# so lagging mirrors or replayed repo metadata can't ship a vulnerable PHP.
- name: Resolve current PHP release from php.net
id: phpmin
# Values reach the script via env, never via ${{ }} inside run: (a
# crafted string would otherwise be shell); outputs are regex-checked.
env:
PHPVER: ${{ matrix.phpver }}
run: |
set -euo pipefail
v="${{ matrix.phpver }}"; branch="${v:0:1}.${v:1}"
v="$PHPVER"; branch="${v:0:1}.${v:1}"
json=$(curl -fsS --retry 5 --max-time 60 "https://www.php.net/releases/index.php?json&max=1&version=${branch}")
min=$(printf '%s' "$json" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(next(iter(d)) if isinstance(d,dict) and d else "")')
if [ -z "$min" ]; then echo "::error::no php.net release found for ${branch}"; exit 1; fi
[[ "$min" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::php.net returned a malformed version for ${branch}"; exit 1; }
echo "php.net current ${branch}: ${min}"
echo "min=${min}" >> "$GITHUB_OUTPUT"
@@ -222,7 +236,9 @@ jobs:
# the package-upgrade layers consume CACHE_BUST and can never be reused.
- name: Cache-bust value
id: bust
run: echo "value=$(date -u +%Y%m%dT%H%M%SZ)-${{ gitea.run_id }}" >> "$GITHUB_OUTPUT"
env:
RUN_ID: ${{ gitea.run_id }}
run: echo "value=$(date -u +%Y%m%dT%H%M%SZ)-${RUN_ID}" >> "$GITHUB_OUTPUT"
# Ask the LiteSpeed apt repo (the same one the base image's apt uses) for the
# CURRENT lsphp version. The Dockerfile asserts the installed package equals
@@ -230,14 +246,21 @@ jobs:
# it into the image labels org.whp.php-version / org.whp.lsphp-package-version.
- name: Resolve current lsphp${{ matrix.phpver }} from the LiteSpeed repo
id: lsphp
# HTTPS (rpms.litespeedtech.com serves it; measured 2026-10-21), values via
# env, and both outputs regex-checked before they reach GITHUB_OUTPUT.
env:
PHPVER: ${{ matrix.phpver }}
run: |
set -euo pipefail
idx=$(curl -fsS --retry 3 --max-time 60 http://rpms.litespeedtech.com/debian/dists/noble/main/binary-amd64/Packages)
pkg=$(printf '%s\n' "$idx" | awk -v p="lsphp${{ matrix.phpver }}" '/^Package: /{n=$2} /^Version: / && n==p {print $2}' | sort -V | tail -n1)
if [ -z "$pkg" ]; then echo "::error::lsphp${{ matrix.phpver }} not found in the LiteSpeed repo index"; exit 1; fi
echo "lsphp${{ matrix.phpver }} in repo: $pkg (PHP ${pkg%%-*})"
idx=$(curl -fsS --proto '=https' --retry 3 --max-time 60 https://rpms.litespeedtech.com/debian/dists/noble/main/binary-amd64/Packages)
pkg=$(printf '%s\n' "$idx" | awk -v p="lsphp${PHPVER}" '/^Package: /{n=$2} /^Version: / && n==p {print $2}' | sort -V | tail -n1)
if [ -z "$pkg" ]; then echo "::error::lsphp${PHPVER} not found in the LiteSpeed repo index"; exit 1; fi
php="${pkg%%-*}"
[[ "$pkg" =~ ^[0-9][0-9A-Za-z.+~:-]*$ ]] || { echo "::error::malformed lsphp${PHPVER} package version in the repo index"; exit 1; }
[[ "$php" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::malformed PHP version '${php}' derived from lsphp${PHPVER} ${pkg}"; exit 1; }
echo "lsphp${PHPVER} in repo: $pkg (PHP ${php})"
echo "pkg=$pkg" >> "$GITHUB_OUTPUT"
echo "php=${pkg%%-*}" >> "$GITHUB_OUTPUT"
echo "php=${php}" >> "$GITHUB_OUTPUT"
- name: Login to Gitea
uses: docker/login-action@v3
@@ -252,12 +275,15 @@ jobs:
# the job goes green again by itself once the LiteSpeed repo catches up.
# Fail-open if the tag cannot be inspected (first build, registry blip).
- name: Do not roll back a newer php.net emergency image
env:
PHPVER: ${{ matrix.phpver }}
NEW_PHP: ${{ steps.lsphp.outputs.php }}
run: |
set -euo pipefail
ref=repo.anhonesthost.net/cloud-hosting-platform/cac-litespeed:php${{ matrix.phpver }}
ref="repo.anhonesthost.net/cloud-hosting-platform/cac-litespeed:php${PHPVER}"
src=$(docker buildx imagetools inspect "$ref" --format '{{ index .Image.Config.Labels "org.whp.lsphp-source" }}' 2>/dev/null || true)
cur=$(docker buildx imagetools inspect "$ref" --format '{{ index .Image.Config.Labels "org.whp.php-version" }}' 2>/dev/null || true)
new='${{ steps.lsphp.outputs.php }}'
new="$NEW_PHP"
echo "published $ref: source=${src:-?} php=${cur:-?}; this build: packaged $new"
if [ "$src" = "php.net" ] && [ "$cur" != "$new" ] && [ "$(printf '%s\n%s\n' "$cur" "$new" | sort -V | tail -n1)" = "$cur" ]; then
echo "::error::$ref is a php.net emergency build of PHP $cur; LiteSpeed still ships $new. Not overwriting it with an older PHP. Re-run lsphp-source-emergency.yaml for a newer fix, or wait for LiteSpeed."
@@ -313,7 +339,9 @@ jobs:
# the package-upgrade layers consume CACHE_BUST and can never be reused.
- name: Cache-bust value
id: bust
run: echo "value=$(date -u +%Y%m%dT%H%M%SZ)-${{ gitea.run_id }}" >> "$GITHUB_OUTPUT"
env:
RUN_ID: ${{ gitea.run_id }}
run: echo "value=$(date -u +%Y%m%dT%H%M%SZ)-${RUN_ID}" >> "$GITHUB_OUTPUT"
# Ask the LiteSpeed apt repo (the same one the base image's apt uses) for the
# CURRENT lsphp version. The Dockerfile asserts the installed package equals
@@ -321,14 +349,21 @@ jobs:
# it into the image labels org.whp.php-version / org.whp.lsphp-package-version.
- name: Resolve current lsphp${{ matrix.phpver }} from the LiteSpeed repo
id: lsphp
# HTTPS (rpms.litespeedtech.com serves it; measured 2026-10-21), values via
# env, and both outputs regex-checked before they reach GITHUB_OUTPUT.
env:
PHPVER: ${{ matrix.phpver }}
run: |
set -euo pipefail
idx=$(curl -fsS --retry 3 --max-time 60 http://rpms.litespeedtech.com/debian/dists/noble/main/binary-amd64/Packages)
pkg=$(printf '%s\n' "$idx" | awk -v p="lsphp${{ matrix.phpver }}" '/^Package: /{n=$2} /^Version: / && n==p {print $2}' | sort -V | tail -n1)
if [ -z "$pkg" ]; then echo "::error::lsphp${{ matrix.phpver }} not found in the LiteSpeed repo index"; exit 1; fi
echo "lsphp${{ matrix.phpver }} in repo: $pkg (PHP ${pkg%%-*})"
idx=$(curl -fsS --proto '=https' --retry 3 --max-time 60 https://rpms.litespeedtech.com/debian/dists/noble/main/binary-amd64/Packages)
pkg=$(printf '%s\n' "$idx" | awk -v p="lsphp${PHPVER}" '/^Package: /{n=$2} /^Version: / && n==p {print $2}' | sort -V | tail -n1)
if [ -z "$pkg" ]; then echo "::error::lsphp${PHPVER} not found in the LiteSpeed repo index"; exit 1; fi
php="${pkg%%-*}"
[[ "$pkg" =~ ^[0-9][0-9A-Za-z.+~:-]*$ ]] || { echo "::error::malformed lsphp${PHPVER} package version in the repo index"; exit 1; }
[[ "$php" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::malformed PHP version '${php}' derived from lsphp${PHPVER} ${pkg}"; exit 1; }
echo "lsphp${PHPVER} in repo: $pkg (PHP ${php})"
echo "pkg=$pkg" >> "$GITHUB_OUTPUT"
echo "php=${pkg%%-*}" >> "$GITHUB_OUTPUT"
echo "php=${php}" >> "$GITHUB_OUTPUT"
- name: Login to Gitea
uses: docker/login-action@v3
@@ -343,12 +378,15 @@ jobs:
# the job goes green again by itself once the LiteSpeed repo catches up.
# Fail-open if the tag cannot be inspected (first build, registry blip).
- name: Do not roll back a newer php.net emergency image
env:
PHPVER: ${{ matrix.phpver }}
NEW_PHP: ${{ steps.lsphp.outputs.php }}
run: |
set -euo pipefail
ref=repo.anhonesthost.net/cloud-hosting-platform/cac-lsphp:php${{ matrix.phpver }}
ref="repo.anhonesthost.net/cloud-hosting-platform/cac-lsphp:php${PHPVER}"
src=$(docker buildx imagetools inspect "$ref" --format '{{ index .Image.Config.Labels "org.whp.lsphp-source" }}' 2>/dev/null || true)
cur=$(docker buildx imagetools inspect "$ref" --format '{{ index .Image.Config.Labels "org.whp.php-version" }}' 2>/dev/null || true)
new='${{ steps.lsphp.outputs.php }}'
new="$NEW_PHP"
echo "published $ref: source=${src:-?} php=${cur:-?}; this build: packaged $new"
if [ "$src" = "php.net" ] && [ "$cur" != "$new" ] && [ "$(printf '%s\n%s\n' "$cur" "$new" | sort -V | tail -n1)" = "$cur" ]; then
echo "::error::$ref is a php.net emergency build of PHP $cur; LiteSpeed still ships $new. Not overwriting it with an older PHP. Re-run lsphp-source-emergency.yaml for a newer fix, or wait for LiteSpeed."
+22 -12
View File
@@ -49,6 +49,8 @@ jobs:
IN_VERSIONS: ${{ inputs.php_versions }}
IN_IMAGES: ${{ inputs.images }}
IN_PROD: ${{ format('{0}', inputs.push_production_tags) }}
PHPVER: ${{ matrix.phpver }}
IMG: ${{ matrix.image }}
run: |
set -euo pipefail
case "$IN_IMAGES" in both|lsphp|litespeed) ;; *) echo "::error::images must be both|lsphp|litespeed, got '$IN_IMAGES'"; exit 1 ;; esac
@@ -56,16 +58,17 @@ jobs:
v=""
for pair in $IN_VERSIONS; do
[[ "$pair" =~ ^(8[1-5])=([0-9]+\.[0-9]+\.[0-9]+)$ ]] || { echo "::error::bad php_versions entry '$pair' (want NN=X.Y.Z, NN in 81..85)"; exit 1; }
[ "${BASH_REMATCH[1]}" = '${{ matrix.phpver }}' ] && v="${BASH_REMATCH[2]}"
[ "${BASH_REMATCH[1]}" = "$PHPVER" ] && v="${BASH_REMATCH[2]}"
done
echo "prod=$IN_PROD" >> "$GITHUB_OUTPUT"
if [ -z "$v" ] || { [ "$IN_IMAGES" != both ] && [ "$IN_IMAGES" != '${{ matrix.image }}' ]; }; then
echo "not requested: php${{ matrix.phpver }} / ${{ matrix.image }}"; echo "skip=1" >> "$GITHUB_OUTPUT"; exit 0
if [ -z "$v" ] || { [ "$IN_IMAGES" != both ] && [ "$IN_IMAGES" != "$IMG" ]; }; then
echo "not requested: php${PHPVER} / ${IMG}"; echo "skip=1" >> "$GITHUB_OUTPUT"; exit 0
fi
echo "skip=0" >> "$GITHUB_OUTPUT"
echo "version=$v" >> "$GITHUB_OUTPUT"
echo "image_name=cac-${{ matrix.image }}" >> "$GITHUB_OUTPUT"
echo "dockerfile=./Dockerfile.${{ matrix.image }}" >> "$GITHUB_OUTPUT"
case "$IMG" in lsphp|litespeed) ;; *) echo "::error::unexpected matrix image '$IMG'"; exit 1 ;; esac
echo "image_name=cac-${IMG}" >> "$GITHUB_OUTPUT"
echo "dockerfile=./Dockerfile.${IMG}" >> "$GITHUB_OUTPUT"
- name: Checkout
if: steps.t.outputs.skip == '0'
@@ -77,26 +80,33 @@ jobs:
- name: Resolve php.net release + packaged lsphp
if: steps.t.outputs.skip == '0'
id: v
env:
V: ${{ steps.t.outputs.version }}
PHPVER: ${{ matrix.phpver }}
RUN_ID: ${{ gitea.run_id }}
run: |
set -euo pipefail
v='${{ steps.t.outputs.version }}'
v="$V"
[[ "$v" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::malformed version '$v'"; exit 1; }
json=$(curl -fsS --retry 3 --max-time 60 "https://www.php.net/releases/index.php?json&version=$v")
sha=$(printf '%s' "$json" | python3 -c 'import json,sys; d=json.load(sys.stdin); v=sys.argv[1]; print(next((s["sha256"] for s in d.get("source", []) if s.get("filename")=="php-%s.tar.xz"%v), ""))' "$v")
[ -n "$sha" ] || { echo "::error::php.net has no release php-$v.tar.xz (EOL branch? typo?): $json"; exit 1; }
[[ "$sha" =~ ^[0-9a-f]{64}$ ]] || { echo "::error::no sha256 for php-$v.tar.xz in the php.net releases JSON"; exit 1; }
idx=$(curl -fsS --retry 3 --max-time 60 http://rpms.litespeedtech.com/debian/dists/noble/main/binary-amd64/Packages)
pkg=$(printf '%s\n' "$idx" | awk -v p="lsphp${{ matrix.phpver }}" '/^Package: /{n=$2} /^Version: / && n==p {print $2}' | sort -V | tail -n1)
[ -n "$pkg" ] || { echo "::error::lsphp${{ matrix.phpver }} not in the LiteSpeed repo index"; exit 1; }
idx=$(curl -fsS --proto '=https' --retry 3 --max-time 60 https://rpms.litespeedtech.com/debian/dists/noble/main/binary-amd64/Packages)
pkg=$(printf '%s\n' "$idx" | awk -v p="lsphp${PHPVER}" '/^Package: /{n=$2} /^Version: / && n==p {print $2}' | sort -V | tail -n1)
[ -n "$pkg" ] || { echo "::error::lsphp${PHPVER} not in the LiteSpeed repo index"; exit 1; }
[[ "$pkg" =~ ^[0-9][0-9A-Za-z.+~:-]*$ ]] || { echo "::error::malformed lsphp${PHPVER} package version in the repo index"; exit 1; }
pphp=${pkg%%-*}
[[ "$pphp" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::malformed PHP version '$pphp' from lsphp${PHPVER} $pkg"; exit 1; }
if [ "$(printf '%s\n%s\n' "$pphp" "$v" | sort -V | tail -n1)" != "$v" ] || [ "$pphp" = "$v" ]; then
echo "::error::LiteSpeed already ships lsphp${{ matrix.phpver }} $pkg (PHP $pphp) >= $v — use the normal build (push to trunk / dispatch build-push.yaml)."
echo "::error::LiteSpeed already ships lsphp${PHPVER} $pkg (PHP $pphp) >= $v — use the normal build (push to trunk / dispatch build-push.yaml)."
exit 1
fi
echo "php.net $v sha256=$sha ; LiteSpeed lsphp${{ matrix.phpver }}=$pkg"
echo "php.net $v sha256=$sha ; LiteSpeed lsphp${PHPVER}=$pkg"
echo "sha=$sha" >> "$GITHUB_OUTPUT"
echo "pkg=$pkg" >> "$GITHUB_OUTPUT"
echo "pkgphp=$pphp" >> "$GITHUB_OUTPUT"
echo "bust=$(date -u +%Y%m%dT%H%M%SZ)-${{ gitea.run_id }}" >> "$GITHUB_OUTPUT"
echo "bust=$(date -u +%Y%m%dT%H%M%SZ)-${RUN_ID}" >> "$GITHUB_OUTPUT"
- name: Set up Docker Buildx
if: steps.t.outputs.skip == '0'
+1
View File
@@ -59,6 +59,7 @@ die() { echo "FATAL(lsphp-src-build): $*" >&2; exit 1; }
log() { echo "lsphp-src-build: $*"; }
PHPVER="${PHPVER:?PHPVER required}"
[[ "$PHPVER" =~ ^[0-9]{2}$ ]] || die "PHPVER='$PHPVER' is not NN."
if [ -z "${PHP_SRC_VERSION:-}" ]; then
[ -z "${PHP_SRC_SHA256:-}" ] || die "PHP_SRC_SHA256 given without PHP_SRC_VERSION."
log "PHP_SRC_VERSION empty — packaged lsphp build, nothing to do."
+1
View File
@@ -37,6 +37,7 @@ MODE="${1:?usage: lsphp-src-overlay.sh runtime|dev PHPVER}"
PHPVER="${2:?usage: lsphp-src-overlay.sh runtime|dev PHPVER}"
OUT="${3:-/lsphp-src-out}"
case "$MODE" in runtime|dev) ;; *) echo "FATAL(lsphp-src-overlay): mode must be runtime|dev" >&2; exit 1 ;; esac
[[ "$PHPVER" =~ ^[0-9]{2}$ ]] || { echo "FATAL(lsphp-src-overlay): PHPVER='$PHPVER' is not NN" >&2; exit 1; }
die() { echo "FATAL(lsphp-src-overlay): $*" >&2; exit 1; }
log() { echo "lsphp-src-overlay[$MODE]: $*"; }