From 34074fce191ed57cec222a7492a5e8541358d300 Mon Sep 17 00:00:00 2001 From: Neil Dewhurst Date: Tue, 14 Jul 2026 19:37:37 +0100 Subject: [PATCH 1/2] Harden docs-deploy-surge.yml against artifact poisoning (sync from docs-template) Syncs .github/workflows/docs-deploy-surge.yml with the canonical version in neo4j/docs-template to resolve the critical CodeQL "artifact poisoning" alert: - add a validate-changelog step that rejects a symlinked changelog before it is read into the PR comment (prevents arbitrary-file-read from a poisoned artifact) - use process.env.RUN_ID instead of ${{ env.RUN_ID }} in the github-script step (avoids GitHub Actions expression injection) - comment/whitespace tidy-ups to match the template Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/docs-deploy-surge.yml | 32 ++++++++++++++++++++----- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docs-deploy-surge.yml b/.github/workflows/docs-deploy-surge.yml index 9278cd6..aa25d43 100644 --- a/.github/workflows/docs-deploy-surge.yml +++ b/.github/workflows/docs-deploy-surge.yml @@ -1,8 +1,8 @@ # Use this starter workflow to deploy HTML generated by Antora to surge.sh # Docs are published at --.surge.sh -# +# # By default, this workflow runs on completion of a workflow called "Verify docs PR" -# +# # This workflow expects the triggering workflow to generate an artifact called "docs" # - update the reference to "docs" and "docs.zip" in this workflow if your triggering workflow generates an artifact with a different name @@ -22,7 +22,7 @@ on: - completed jobs: - # [Optional] Restrict automatic dpeloyment to PRs from the upstream repo + # [Optional] Restrict automatic deployment to PRs from the upstream repo # For fork PRs, requires manual approval via the "preview" environment. # For PRs from the main repository this job is skipped and deploy-docs runs immediately. # Setup: create a "preview" environment in Settings → Environments with required reviewers. @@ -56,7 +56,7 @@ jobs: var artifacts = await github.rest.actions.listWorkflowRunArtifacts({ owner: context.repo.owner, repo: context.repo.repo, - run_id: ${{ env.RUN_ID }}, + run_id: process.env.RUN_ID, }); var matchArtifactDocs = artifacts.data.artifacts.filter((artifact) => { @@ -132,6 +132,26 @@ jobs: cd "$ARTIFACT_DIR" unzip changelog.zip + # The changelog file is built from untrusted PR content and its contents + # are posted to the PR as a comment. A malicious artifact could make + # `changelog` a symlink pointing at an arbitrary file the runner can read, + # turning the comment step into an arbitrary-file-read. Reject anything + # that isn't a plain regular file before we read it. + - id: validate-changelog + name: Validate changelog is a regular file + if: ${{ steps.find-changelog.outputs.has-changelog == 'true' }} + env: + CHANGELOG_FILE: ${{ runner.temp }}/artifacts/changelog/changelog + run: | + if [ -L "$CHANGELOG_FILE" ]; then + echo "Security Alert: changelog is a symlink — refusing!" + exit 1 + fi + if [ ! -f "$CHANGELOG_FILE" ]; then + echo "changelog missing or not a regular file" + exit 1 + fi + - id: get-deploy-id name: Get deploy ID env: @@ -150,7 +170,7 @@ jobs: run: | deployurl=$ORG-$REPO-$DEPLOYID.surge.sh echo "deploy-url=$deployurl" >> $GITHUB_OUTPUT - + - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 with: node-version: lts/* @@ -165,7 +185,7 @@ jobs: mkdir -p "$DOCS_DEPLOY_DIR" # Copy only the built docs into a clean directory for deployment cp -R "$DOCS_SRC_DIR"/. "$DOCS_DEPLOY_DIR"/ - + - id: surge-deploy name: Deploy docs to surge shell: bash From 3dff04d025e3ac30713fe13223210d250965a132 Mon Sep 17 00:00:00 2001 From: Neil Dewhurst Date: Mon, 20 Jul 2026 16:35:11 +0100 Subject: [PATCH 2/2] Update deploy-surge to extended artifact validation (sync with docs-template) Refreshes the sync to the now-canonical docs-template version (docs-template#59): replaces the old `unzip -l | grep '../'` suspicious-path-check with the zipinfo loop that also rejects empty archives, absolute paths, and Windows-style backslash separators. No branch/path filters changed. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/docs-deploy-surge.yml | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docs-deploy-surge.yml b/.github/workflows/docs-deploy-surge.yml index aa25d43..682c6cf 100644 --- a/.github/workflows/docs-deploy-surge.yml +++ b/.github/workflows/docs-deploy-surge.yml @@ -86,13 +86,32 @@ jobs: - id: suspicious-path-check name: Suspicious paths check + shell: bash env: ARTIFACT_DIR: ${{ runner.temp }}/artifacts/docs run: | + set -euo pipefail cd "$ARTIFACT_DIR" - if unzip -l docs.zip | grep -q "\.\./"; then + + mapfile -t ZIP_ENTRIES < <(zipinfo -1 docs.zip) + if [ "${#ZIP_ENTRIES[@]}" -eq 0 ]; then + echo "docs.zip is empty" exit 1 fi + for entry in "${ZIP_ENTRIES[@]}"; do + if [[ "$entry" =~ ^/ ]]; then + echo "Blocked absolute path in artifact: $entry" + exit 1 + fi + if [[ "$entry" =~ (^|/)\.\.(/|$) ]]; then + echo "Blocked path traversal in artifact: $entry" + exit 1 + fi + if [[ "$entry" == *\\* ]]; then + echo "Blocked Windows-style path separator in artifact: $entry" + exit 1 + fi + done - id: hidden-files-check name: Hidden files check