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: 1 addition & 1 deletion cli/dispatcher/shared-inputs-stamp.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"schemaVersion": 1,
"sharedInputsHash": "4cf6c7f5a03b5a1d2d0046ed035ddc4db5158516"
"sharedInputsHash": "d2f7e8475cdf15e5334b349420182ca28dba8b22"
}
23 changes: 23 additions & 0 deletions scripts/install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,29 @@ $InstallDir = if ($env:ULOOP_INSTALL_DIR) {
}
$AssetName = "uloop-dispatcher-windows-amd64.zip"

function Test-UloopVersionFormat {
param(
[Parameter(Mandatory = $true)]
[string]$Candidate
)

# Why: mirror install.sh — Uri.Combine and .NET's WebClient do not
# normalize away path segments, so a value like
# "../../evil/repo/releases/download/v1" would traverse out of the
# expected release path. Fail-close on anything that is not one of the
# two well-known channel selectors or a semver-shaped tag.
if ($Candidate -eq $LatestVersion -or $Candidate -eq $LatestBetaVersion) {
return
}
$Pattern = '^(dispatcher-v|uloop-project-runner-v|v)?[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$'
if ($Candidate -match $Pattern) {
return
}
throw "Invalid ULOOP_VERSION: $Candidate. Expected 'latest', 'latest-beta', or a semver tag such as '3.0.0-beta.5' / 'dispatcher-v3.0.0-beta.5'."
}

Test-UloopVersionFormat -Candidate $Version

function Find-LatestAssetUrl {
param(
[Parameter(Mandatory = $true)]
Expand Down
40 changes: 40 additions & 0 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,46 @@ VERSION="${ULOOP_VERSION:-latest}"
LATEST_VERSION="latest"
LATEST_BETA_VERSION="latest-beta"

# Why: install.sh interpolates $VERSION into
# "https://github.com/$REPOSITORY/releases/download/$VERSION/..." without
# curl normalizing dot segments, so a value like
# "../../evil/repo/releases/download/v1" would break out of the intended
# release path even though the ambient attacker who can already set env
# usually has bigger wins. Fail-close early on any value that is neither
# the two well-known channel selectors nor a semantic-version-shaped tag,
# so downstream users of $VERSION (asset URL, ULOOP_VERSION passthrough,
# release tag prefixing) never see a value they weren't designed for.
validate_uloop_version() {
candidate=$1
if [ "$candidate" = "$LATEST_VERSION" ] || [ "$candidate" = "$LATEST_BETA_VERSION" ]; then
return
fi
# Why: `grep -Eq` matches per line, so a value like
# printf '../evil\n3.0.0' would pass the ERE below on its second line
# while leaking the embedded newline into the URL builder downstream.
# Reject anything outside the semver tag alphabet at the whole-string
# level first — `case` sees the value as one string and cannot be
# smuggled past with embedded newlines or NULs.
case $candidate in
''|*[!0-9A-Za-z.+-]*)
echo "Invalid ULOOP_VERSION: $candidate" >&2
echo "Expected 'latest', 'latest-beta', or a semver tag such as '3.0.0-beta.5' / 'dispatcher-v3.0.0-beta.5'." >&2
exit 1
;;
esac
# POSIX ERE: optional dispatcher-v / uloop-project-runner-v / v prefix,
# then MAJOR.MINOR.PATCH, then optional -prerelease.identifiers or
# +build.metadata.
if printf '%s' "$candidate" | grep -Eq '^(dispatcher-v|uloop-project-runner-v|v)?[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$'; then
return
fi
echo "Invalid ULOOP_VERSION: $candidate" >&2
echo "Expected 'latest', 'latest-beta', or a semver tag such as '3.0.0-beta.5' / 'dispatcher-v3.0.0-beta.5'." >&2
exit 1
}

validate_uloop_version "$VERSION"

report_path_shadowing() {
resolved_uloop=$(command -v uloop 2>/dev/null || true)
expected_uloop="$INSTALL_DIR/$installed_command_name"
Expand Down
73 changes: 73 additions & 0 deletions scripts/test-install-version-format.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/bin/sh
# Verifies install.sh's validate_uloop_version helper accepts every version
# shape the dispatcher self-update flow can emit (latest, latest-beta, bare
# semver, dispatcher-v prefix, project-runner prefix, prerelease + build
# metadata) and rejects values that could break out of the release path.
set -eu

ROOT_DIR=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
INSTALL_SCRIPT="$ROOT_DIR/scripts/install.sh"

extract_function() {
name=$1
awk -v want="$name" '
$0 ~ ("^" want "\\(\\)") { in_fn = 1 }
in_fn { print }
in_fn && $0 == "}" { exit }
' "$INSTALL_SCRIPT"
}

LATEST_VERSION="latest"
LATEST_BETA_VERSION="latest-beta"
eval "$(extract_function validate_uloop_version)"

expect_pass() {
value=$1
if ! (validate_uloop_version "$value") >/dev/null 2>&1; then
echo "FAIL: expected pass for '$value'" >&2
exit 1
fi
}

expect_fail() {
value=$1
if (validate_uloop_version "$value") >/dev/null 2>&1; then
echo "FAIL: expected reject for '$value'" >&2
exit 1
fi
}

# Well-known channel selectors — must always pass.
expect_pass "latest"
expect_pass "latest-beta"

# Bare semver as emitted by --to-version after NormalizeTargetVersion.
expect_pass "3.0.0"
expect_pass "3.0.0-beta.5"
expect_pass "3.0.1-beta.12"
expect_pass "1.2.3-rc.1+build.7"

# Release-tag prefixed values as emitted by DispatcherReleaseTag /
# ProjectRunnerReleaseTag / bare 'v' prefix.
expect_pass "dispatcher-v3.0.0"
expect_pass "dispatcher-v3.0.0-beta.5"
expect_pass "uloop-project-runner-v3.0.0-beta.43"
expect_pass "v3.0.0"

# Path traversal or URL-injection shapes must not survive.
expect_fail "../../evil/repo/releases/download/v1"
expect_fail "3.0.0/../evil"
expect_fail "3.0.0?redirect=evil"
expect_fail "3.0.0#fragment"
expect_fail "3.0.0 3.0.0"
expect_fail ""
expect_fail "not-a-version"
expect_fail "3.0"

# Embedded newline: grep -Eq alone matches per-line, so "evil\n3.0.0" would
# smuggle a URL-poisoning first line past the ERE. The whole-string `case`
# guard in front of the grep must catch this.
expect_fail "$(printf 'evil\n3.0.0')"
expect_fail "$(printf '3.0.0\n../evil')"

echo "OK"