Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,8 @@ test:

### With GitLab Inputs

`setup-ref` accepts ASCII letters, digits, underscores, and hyphens, with single dots or slashes between these groups (for example, `v1.21.1` or `refs/tags/v1.21.1`). The templates and bootstrap scripts reject other values before downloading executable files. The same validation applies to `SETUP_VP_SETUP_REF` when using `.setup-vp-bootstrap` directly. Keep this value under maintainer control because it selects code that runs on the runner.

```yaml
include:
- remote: "https://raw.githubusercontent.com/voidzero-dev/setup-vp/v1.21.1/gitlab/setup-vp.yml"
Expand Down
3 changes: 3 additions & 0 deletions azure/bootstrap.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ function Setup-VpDownload {
}

$setupRef = if ($env:SETUP_VP_SETUP_REF) { $env:SETUP_VP_SETUP_REF } else { 'v1.21.1' }
if ($setupRef -cnotmatch '\A[A-Za-z0-9_-]+([./][A-Za-z0-9_-]+)*\z') {
throw 'setup-vp: invalid setupRef; use a tag, branch or commit SHA with safe ref characters.'
}
$runtimeOut = if ($env:SETUP_VP_RUNTIME_OUT) {
$env:SETUP_VP_RUNTIME_OUT
} else {
Expand Down
4 changes: 4 additions & 0 deletions azure/bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ setup_vp_download() {
}

SETUP_VP_SETUP_REF="${SETUP_VP_SETUP_REF:-v1.21.1}"
if [[ ! "$SETUP_VP_SETUP_REF" =~ ^[A-Za-z0-9_-]+([./][A-Za-z0-9_-]+)*$ ]]; then
echo 'setup-vp: invalid setupRef; use a tag, branch or commit SHA with safe ref characters.' >&2
exit 1
fi
SETUP_VP_RUNTIME_OUT="${SETUP_VP_RUNTIME_OUT:-${TMPDIR:-/tmp}/setup-vp-azure/dist/azure/index.mjs}"
setup_vp_runtime_dir="$(dirname "$SETUP_VP_RUNTIME_OUT")"
setup_vp_chunk_dir="$(dirname "$setup_vp_runtime_dir")"
Expand Down
3 changes: 3 additions & 0 deletions gitlab/bootstrap.ps1
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
$ErrorActionPreference = 'Stop'
$runtimeNode = (Get-Command node -ErrorAction Stop).Source
$setupRef = if ($env:SETUP_VP_SETUP_REF) { $env:SETUP_VP_SETUP_REF } else { 'v1.21.1' }
if ($setupRef -cnotmatch '\A[A-Za-z0-9_-]+([./][A-Za-z0-9_-]+)*\z') {
throw 'setup-vp: invalid setup-ref; use a tag, branch or commit SHA with safe ref characters.'
}
$runtime = Join-Path ([IO.Path]::GetTempPath()) ("setup-vp-gitlab-" + [guid]::NewGuid() + ".mjs")
try {
$url = "https://raw.githubusercontent.com/voidzero-dev/setup-vp/$setupRef/dist/gitlab/index.mjs"
Expand Down
4 changes: 4 additions & 0 deletions gitlab/bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ setup_vp_download() {
fi
}
SETUP_VP_SETUP_REF="${SETUP_VP_SETUP_REF:-v1.21.1}"
if [[ ! "$SETUP_VP_SETUP_REF" =~ ^[A-Za-z0-9_-]+([./][A-Za-z0-9_-]+)*$ ]]; then
echo 'setup-vp: invalid setup-ref; use a tag, branch or commit SHA with safe ref characters.' >&2
exit 1
fi
setup_vp_runtime_dir="$(mktemp -d "${TMPDIR:-/tmp}/setup-vp-gitlab-runtime.XXXXXX")"
setup_vp_runtime_tmp="$setup_vp_runtime_dir/index.mjs"
trap 'rm -f "$setup_vp_runtime_tmp"; rmdir "$setup_vp_runtime_dir"' EXIT
Expand Down
4 changes: 4 additions & 0 deletions gitlab/setup-vp-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,16 @@ spec:
setup-ref:
description: "setup-vp ref used to download the GitLab bootstrap and compiled runtime. Pin this to the same tag or commit as the remote template; the default is the latest release when this template was published."
default: "v1.21.1"
regex: '\A[A-Za-z0-9_-]+([./][A-Za-z0-9_-]+)*\z'
---
.setup-vp-bootstrap:
before_script:
- |
$ErrorActionPreference = 'Stop'
if (-not $env:SETUP_VP_SETUP_REF) { $env:SETUP_VP_SETUP_REF = 'v1.21.1' }
if ($env:SETUP_VP_SETUP_REF -cnotmatch '\A[A-Za-z0-9_-]+([./][A-Za-z0-9_-]+)*\z') {
throw 'setup-vp: invalid setup-ref; use a tag, branch or commit SHA with safe ref characters.'
}
$bootstrap = Join-Path ([IO.Path]::GetTempPath()) ("setup-vp-bootstrap-" + [guid]::NewGuid() + ".ps1")
$envFile = Join-Path ([IO.Path]::GetTempPath()) ("setup-vp-env-" + [guid]::NewGuid() + ".ps1")
try {
Expand Down
9 changes: 9 additions & 0 deletions gitlab/setup-vp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ spec:
setup-ref:
description: "setup-vp ref used to download the GitLab bootstrap and compiled runtime. Pin this to the same tag or commit as the remote template; the default is the latest release when this template was published."
default: "v1.21.1"
regex: '\A[A-Za-z0-9_-]+([./][A-Za-z0-9_-]+)*\z'
---
.setup-vp-bootstrap:
before_script:
Expand Down Expand Up @@ -81,6 +82,14 @@ spec:
export SETUP_VP_SCOPE="${SETUP_VP_SCOPE:-}"
export SETUP_VP_SETUP_REF="${SETUP_VP_SETUP_REF:-v1.21.1}"

# Keep this check POSIX-compatible: the runner may use sh before Bash starts.
case "$SETUP_VP_SETUP_REF" in
*[!ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_./-]*|[./]*|*[./]|*..*|*./*|*/.*|*//*)
echo 'setup-vp: invalid setup-ref; use a tag, branch or commit SHA with safe ref characters.' >&2
exit 1
;;
esac

setup_vp_bootstrap_tmp="$(mktemp "${TMPDIR:-/tmp}/setup-vp-gitlab-bootstrap.XXXXXX")"
setup_vp_env_tmp="$(mktemp "${TMPDIR:-/tmp}/setup-vp-gitlab-env.XXXXXX")"
chmod 600 "$setup_vp_env_tmp"
Expand Down
173 changes: 173 additions & 0 deletions src/setup-ref.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import { spawnSync } from "node:child_process";
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vite-plus/test";
import { parseAllDocuments } from "yaml";

const { version } = JSON.parse(
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
) as { version: string };

const safeRefs = [`v${version}`, "a".repeat(40), "main", "refs/tags/v1.21.1", "feature/test_ref-1"];
const unsafeRefs = [
"v1/../../../attacker/repo/main",
"../other-repo/main",
"refs/../main",
"refs/./main",
"refs//main",
"/main",
"main/",
".main",
"main.",
"v1..2",
"refs./main",
"refs/.main",
"v1/%2e%2e/%2e%2e/attacker/repo/main",
"v1/%252e%252e/attacker/repo/main",
"v1%2f..",
"refs\\..\\main",
"v1?query",
"v1#fragment",
"v1 with spaces",
"v1\t",
"v1\n",
"v1\r\n",
"v1\nmain",
"v1\u0001",
"rélease",
"v1;exit 0",
'$(printf injected > "$SETUP_VP_TEST_EXECUTED")',
'`printf injected > "$SETUP_VP_TEST_EXECUTED"`',
"$(Set-Content -LiteralPath $env:SETUP_VP_TEST_EXECUTED -Value injected)",
];

type Shell = "sh" | "bash" | "powershell";

function readSource(file: string) {
return readFileSync(new URL(`../${file}`, import.meta.url), "utf8");
}

function readTemplate(file: string) {
return parseAllDocuments(readSource(file), {
customTags: [{ tag: "!reference", collection: "seq", resolve: (value) => value }],
});
}

function runScript(file: string, shell: Shell, setupRef: string | undefined) {
const directory = mkdtempSync(join(tmpdir(), "setup-vp-setup-ref-"));
const download = join(directory, "download");
const executed = join(directory, "executed");
const isTemplate = file.endsWith(".yml");
const source = isTemplate
? readTemplate(file)[1]!.toJSON()[".setup-vp-bootstrap"].before_script[0]
: readSource(file);
const payload = isTemplate
? shell === "powershell"
? '[IO.File]::WriteAllText($env:SETUP_VP_TEST_EXECUTED, "executed")'
: 'printf executed > "$SETUP_VP_TEST_EXECUTED"'
: 'import { writeFileSync } from "node:fs"; writeFileSync(process.env.SETUP_VP_TEST_EXECUTED, "executed");';
const prelude =
shell === "powershell"
? `
function Invoke-WebRequest {
param([string]$Uri, [string]$OutFile, [int]$TimeoutSec)
[IO.File]::WriteAllText($env:SETUP_VP_TEST_DOWNLOAD, $Uri)
Copy-Item -LiteralPath $env:SETUP_VP_TEST_PAYLOAD -Destination $OutFile
}
`
: `
curl() {
printf '%s' "$6" > "$SETUP_VP_TEST_DOWNLOAD"
cp "$SETUP_VP_TEST_PAYLOAD" "$8"
}
wget() {
printf '%s' "$8" > "$SETUP_VP_TEST_DOWNLOAD"
cp "$SETUP_VP_TEST_PAYLOAD" "$7"
}
`;
const scriptPath = join(directory, shell === "powershell" ? "test.ps1" : "test.sh");
const payloadPath = join(directory, "payload");
writeFileSync(scriptPath, prelude + source);
writeFileSync(payloadPath, payload);
try {
const result = spawnSync(
shell === "powershell" ? "powershell.exe" : shell,
shell === "powershell"
? ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", scriptPath]
: [scriptPath],
{
encoding: "utf8",
timeout: 10_000,
env: {
...process.env,
TMPDIR: directory,
AGENT_TEMPDIRECTORY: directory,
SETUP_VP_SETUP_REF: setupRef,
SETUP_VP_RUNTIME_OUT: join(directory, "dist", "azure", "index.mjs"),
SETUP_VP_TEST_DOWNLOAD: download,
SETUP_VP_TEST_EXECUTED: executed,
SETUP_VP_TEST_PAYLOAD: payloadPath,
},
},
);
expect(result.error).toBeUndefined();
return {
...result,
download: existsSync(download) ? readFileSync(download, "utf8") : undefined,
executed: existsSync(executed),
};
} finally {
rmSync(directory, { recursive: true, force: true });
}
}

describe("setup-ref download boundaries", () => {
it.each(["gitlab/setup-vp.yml", "gitlab/setup-vp-windows.yml"])(
"%s constrains inputs before script interpolation",
(file) => {
const input = readTemplate(file)[0]!.toJSON().spec.inputs["setup-ref"];
expect(input.regex).toBe("\\A[A-Za-z0-9_-]+([./][A-Za-z0-9_-]+)*\\z");
},
);

const targets: Array<[string, Shell, string]> = [
["gitlab/setup-vp.yml", "sh", "gitlab/bootstrap.sh"],
["gitlab/setup-vp.yml", "bash", "gitlab/bootstrap.sh"],
["gitlab/setup-vp-windows.yml", "powershell", "gitlab/bootstrap.ps1"],
["gitlab/bootstrap.sh", "bash", "dist/gitlab/index.mjs"],
["gitlab/bootstrap.ps1", "powershell", "dist/gitlab/index.mjs"],
["azure/bootstrap.sh", "bash", "dist/azure/index.mjs"],
["azure/bootstrap.ps1", "powershell", "dist/azure/index.mjs"],
];
for (const [file, shell, downloadPath] of targets) {
// Exercise the same native shells as each CI runner. Windows coverage runs
// in the existing test-unit matrix in .github/workflows/test.yml.
describe.skipIf((shell === "powershell") !== (process.platform === "win32"))(
`${file} (${shell})`,
() => {
it.each([...safeRefs, "", undefined])("downloads and executes safe ref %j", (setupRef) => {
const result = runScript(file, shell, setupRef);
expect(result.status, result.stderr).toBe(0);
expect(result.download).toBe(
`https://raw.githubusercontent.com/voidzero-dev/setup-vp/${setupRef || `v${version}`}/${downloadPath}`,
);
expect(result.executed).toBe(true);
});

it.each(unsafeRefs)(
"rejects unsafe ref %j before any download or execution",
(setupRef) => {
const result = runScript(file, shell, setupRef);
expect(result.status).not.toBe(0);
expect(result.stderr).toContain(
file.startsWith("azure/") ? "invalid setupRef" : "invalid setup-ref",
);
expect(result.download).toBeUndefined();
expect(result.executed).toBe(false);
},
);
},
);
}
});
3 changes: 3 additions & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ const minifyOptions = {
export default defineConfig({
test: {
include: ["src/**/*.test.ts"],
// PowerShell and Git Bash subprocesses can exceed five seconds on Windows
// CI runners. Allow startup overhead beyond the subprocess timeout.
testTimeout: process.platform === "win32" ? 15_000 : 5_000,
},
staged: {
"*": "vp check --fix",
Expand Down
Loading