Skip to content

Commit 8ada04f

Browse files
authored
DOCOPS-160 Harden docs-deploy-surge.yml against artifact poisoning (sync from docs-template) (#14)
Syncs .github/workflows/docs-deploy-surge.yml to the canonical neo4j/docs-template version to resolve the critical CodeQL artifact-poisoning alert. Adds: - validate-changelog step (rejects a symlinked changelog before it is read into the PR comment – prevents arbitrary-file-read from a poisoned artifact) - process.env.RUN_ID instead of ${{ env.RUN_ID }} in the github-script step (avoids GitHub Actions expression injection) - extended zipinfo suspicious-path-check: rejects empty archives, absolute paths, path traversal (..), and Windows-style backslash separators No branch or path filters changed.
1 parent 59fa1df commit 8ada04f

1 file changed

Lines changed: 46 additions & 7 deletions

File tree

.github/workflows/docs-deploy-surge.yml

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# Use this starter workflow to deploy HTML generated by Antora to surge.sh
22
# Docs are published at <org>-<repo>-<deployid>.surge.sh
3-
#
3+
#
44
# By default, this workflow runs on completion of a workflow called "Verify docs PR"
5-
#
5+
#
66
# This workflow expects the triggering workflow to generate an artifact called "docs"
77
# - update the reference to "docs" and "docs.zip" in this workflow if your triggering workflow generates an artifact with a different name
88

@@ -22,7 +22,7 @@ on:
2222
- completed
2323

2424
jobs:
25-
# [Optional] Restrict automatic dpeloyment to PRs from the upstream repo
25+
# [Optional] Restrict automatic deployment to PRs from the upstream repo
2626
# For fork PRs, requires manual approval via the "preview" environment.
2727
# For PRs from the main repository this job is skipped and deploy-docs runs immediately.
2828
# Setup: create a "preview" environment in Settings → Environments with required reviewers.
@@ -56,7 +56,7 @@ jobs:
5656
var artifacts = await github.rest.actions.listWorkflowRunArtifacts({
5757
owner: context.repo.owner,
5858
repo: context.repo.repo,
59-
run_id: ${{ env.RUN_ID }},
59+
run_id: process.env.RUN_ID,
6060
});
6161
6262
var matchArtifactDocs = artifacts.data.artifacts.filter((artifact) => {
@@ -86,13 +86,32 @@ jobs:
8686
8787
- id: suspicious-path-check
8888
name: Suspicious paths check
89+
shell: bash
8990
env:
9091
ARTIFACT_DIR: ${{ runner.temp }}/artifacts/docs
9192
run: |
93+
set -euo pipefail
9294
cd "$ARTIFACT_DIR"
93-
if unzip -l docs.zip | grep -q "\.\./"; then
95+
96+
mapfile -t ZIP_ENTRIES < <(zipinfo -1 docs.zip)
97+
if [ "${#ZIP_ENTRIES[@]}" -eq 0 ]; then
98+
echo "docs.zip is empty"
9499
exit 1
95100
fi
101+
for entry in "${ZIP_ENTRIES[@]}"; do
102+
if [[ "$entry" =~ ^/ ]]; then
103+
echo "Blocked absolute path in artifact: $entry"
104+
exit 1
105+
fi
106+
if [[ "$entry" =~ (^|/)\.\.(/|$) ]]; then
107+
echo "Blocked path traversal in artifact: $entry"
108+
exit 1
109+
fi
110+
if [[ "$entry" == *\\* ]]; then
111+
echo "Blocked Windows-style path separator in artifact: $entry"
112+
exit 1
113+
fi
114+
done
96115
97116
- id: hidden-files-check
98117
name: Hidden files check
@@ -132,6 +151,26 @@ jobs:
132151
cd "$ARTIFACT_DIR"
133152
unzip changelog.zip
134153
154+
# The changelog file is built from untrusted PR content and its contents
155+
# are posted to the PR as a comment. A malicious artifact could make
156+
# `changelog` a symlink pointing at an arbitrary file the runner can read,
157+
# turning the comment step into an arbitrary-file-read. Reject anything
158+
# that isn't a plain regular file before we read it.
159+
- id: validate-changelog
160+
name: Validate changelog is a regular file
161+
if: ${{ steps.find-changelog.outputs.has-changelog == 'true' }}
162+
env:
163+
CHANGELOG_FILE: ${{ runner.temp }}/artifacts/changelog/changelog
164+
run: |
165+
if [ -L "$CHANGELOG_FILE" ]; then
166+
echo "Security Alert: changelog is a symlink — refusing!"
167+
exit 1
168+
fi
169+
if [ ! -f "$CHANGELOG_FILE" ]; then
170+
echo "changelog missing or not a regular file"
171+
exit 1
172+
fi
173+
135174
- id: get-deploy-id
136175
name: Get deploy ID
137176
env:
@@ -150,7 +189,7 @@ jobs:
150189
run: |
151190
deployurl=$ORG-$REPO-$DEPLOYID.surge.sh
152191
echo "deploy-url=$deployurl" >> $GITHUB_OUTPUT
153-
192+
154193
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
155194
with:
156195
node-version: lts/*
@@ -165,7 +204,7 @@ jobs:
165204
mkdir -p "$DOCS_DEPLOY_DIR"
166205
# Copy only the built docs into a clean directory for deployment
167206
cp -R "$DOCS_SRC_DIR"/. "$DOCS_DEPLOY_DIR"/
168-
207+
169208
- id: surge-deploy
170209
name: Deploy docs to surge
171210
shell: bash

0 commit comments

Comments
 (0)