Compare commits

...

1 Commits

Author SHA1 Message Date
Shivam Mathur 130d8a0fd3 Harden tool checksum verification 2026-07-23 18:22:38 +05:30
6 changed files with 90 additions and 32 deletions
+13
View File
@@ -226,11 +226,15 @@ describe('Tools tests', () => {
${'tool:1.2.3'} | ${'tool:1.2.3'} | ${undefined} | ${undefined} ${'tool:1.2.3'} | ${'tool:1.2.3'} | ${undefined} | ${undefined}
${'tool:1.2.3@sha256:' + 'a'.repeat(64)} | ${'tool:1.2.3'} | ${'sha256:' + 'a'.repeat(64)} | ${undefined} ${'tool:1.2.3@sha256:' + 'a'.repeat(64)} | ${'tool:1.2.3'} | ${'sha256:' + 'a'.repeat(64)} | ${undefined}
${'tool:1.2.3@sha256:' + 'A'.repeat(64)} | ${'tool:1.2.3'} | ${'sha256:' + 'a'.repeat(64)} | ${undefined} ${'tool:1.2.3@sha256:' + 'A'.repeat(64)} | ${'tool:1.2.3'} | ${'sha256:' + 'a'.repeat(64)} | ${undefined}
${'tool:1.2.3@SHA256:' + 'a'.repeat(64)} | ${'tool:1.2.3'} | ${'sha256:' + 'a'.repeat(64)} | ${undefined}
${'tool:1.2.3@sha512:' + 'b'.repeat(128)} | ${'tool:1.2.3'} | ${'sha512:' + 'b'.repeat(128)} | ${undefined} ${'tool:1.2.3@sha512:' + 'b'.repeat(128)} | ${'tool:1.2.3'} | ${'sha512:' + 'b'.repeat(128)} | ${undefined}
${'composer:2.9.8@sha256:' + 'c'.repeat(64)} | ${'composer:2.9.8'} | ${'sha256:' + 'c'.repeat(64)} | ${undefined} ${'composer:2.9.8@sha256:' + 'c'.repeat(64)} | ${'composer:2.9.8'} | ${'sha256:' + 'c'.repeat(64)} | ${undefined}
${'tool:1.2.3@sha256:xyz'} | ${'tool:1.2.3'} | ${undefined} | ${'Invalid sha256 checksum, expected 64 hexadecimal characters'} ${'tool:1.2.3@sha256:xyz'} | ${'tool:1.2.3'} | ${undefined} | ${'Invalid sha256 checksum, expected 64 hexadecimal characters'}
${'tool:1.2.3@sha256:' + 'a'.repeat(63)} | ${'tool:1.2.3'} | ${undefined} | ${'Invalid sha256 checksum, expected 64 hexadecimal characters'} ${'tool:1.2.3@sha256:' + 'a'.repeat(63)} | ${'tool:1.2.3'} | ${undefined} | ${'Invalid sha256 checksum, expected 64 hexadecimal characters'}
${'tool:1.2.3@sha512:' + 'b'.repeat(64)} | ${'tool:1.2.3'} | ${undefined} | ${'Invalid sha512 checksum, expected 128 hexadecimal characters'} ${'tool:1.2.3@sha512:' + 'b'.repeat(64)} | ${'tool:1.2.3'} | ${undefined} | ${'Invalid sha512 checksum, expected 128 hexadecimal characters'}
${'tool:1.2.3@sha384:' + 'b'.repeat(96)} | ${'tool:1.2.3'} | ${undefined} | ${'Unsupported checksum algorithm sha384, expected sha256 or sha512'}
${'tool:1.2.3@md5:' + 'b'.repeat(32)} | ${'tool:1.2.3'} | ${undefined} | ${'Unsupported checksum algorithm md5, expected sha256 or sha512'}
${'tool:1.2.3@sha256' + 'a'.repeat(64)} | ${'tool:1.2.3'} | ${undefined} | ${'Invalid checksum syntax, expected @sha256:<hash> or @sha512:<hash>'}
${'tool:1.0@dev'} | ${'tool:1.0@dev'} | ${undefined} | ${undefined} ${'tool:1.0@dev'} | ${'tool:1.0@dev'} | ${undefined} | ${undefined}
`( `(
'checking extractChecksum: $release', 'checking extractChecksum: $release',
@@ -257,6 +261,11 @@ describe('Tools tests', () => {
${'a, b, composer:v2'} | ${'composer:2, a, b'} ${'a, b, composer:v2'} | ${'composer:2, a, b'}
${'a, b, composer:2.7.1@sha256:' + 'a'.repeat(64)} | ${'composer:2.7.1@sha256:' + 'a'.repeat(64) + ', a, b'} ${'a, b, composer:2.7.1@sha256:' + 'a'.repeat(64)} | ${'composer:2.7.1@sha256:' + 'a'.repeat(64) + ', a, b'}
${'a, b, composer:v2.7.1@sha256:' + 'a'.repeat(64)} | ${'composer:2.7.1@sha256:' + 'a'.repeat(64) + ', a, b'} ${'a, b, composer:v2.7.1@sha256:' + 'a'.repeat(64)} | ${'composer:2.7.1@sha256:' + 'a'.repeat(64) + ', a, b'}
${'a, b, composer:2.7.1@SHA256:' + 'a'.repeat(64)} | ${'composer:2.7.1@SHA256:' + 'a'.repeat(64) + ', a, b'}
${'a, b, composer:2.7.1@sha384:' + 'a'.repeat(96)} | ${'composer:2.7.1@sha384:' + 'a'.repeat(96) + ', a, b'}
${'a, b, composer:2.7.1@md5:' + 'a'.repeat(32)} | ${'composer:2.7.1@md5:' + 'a'.repeat(32) + ', a, b'}
${'a, b, composer:2.7.1@sha256' + 'a'.repeat(64)} | ${'composer:2.7.1@sha256' + 'a'.repeat(64) + ', a, b'}
${'a, b, composer@sha256:' + 'a'.repeat(64)} | ${'composer@sha256:' + 'a'.repeat(64) + ', a, b'}
`('checking filterList $input_list', async ({input_list, filtered_list}) => { `('checking filterList $input_list', async ({input_list, filtered_list}) => {
expect(await tools.filterList(input_list.split(', '))).toStrictEqual( expect(await tools.filterList(input_list.split(', '))).toStrictEqual(
filtered_list.split(', ') filtered_list.split(', ')
@@ -825,6 +834,10 @@ describe('Tools tests', () => {
${'phinx:1.2.3@sha256:' + 'a'.repeat(64)} | ${'linux'} | ${'add_log "$cross" "phinx" "Checksum verification is not supported for phinx"'} ${'phinx:1.2.3@sha256:' + 'a'.repeat(64)} | ${'linux'} | ${'add_log "$cross" "phinx" "Checksum verification is not supported for phinx"'}
${'pecl@sha256:' + 'a'.repeat(64)} | ${'linux'} | ${'add_log "$cross" "pecl" "Checksum verification is not supported for pecl"'} ${'pecl@sha256:' + 'a'.repeat(64)} | ${'linux'} | ${'add_log "$cross" "pecl" "Checksum verification is not supported for pecl"'}
${'phpunit:9.5.0@sha256:invalid'} | ${'linux'} | ${'add_log "$cross" "phpunit" "Invalid sha256 checksum, expected 64 hexadecimal characters"'} ${'phpunit:9.5.0@sha256:invalid'} | ${'linux'} | ${'add_log "$cross" "phpunit" "Invalid sha256 checksum, expected 64 hexadecimal characters"'}
${'composer:2.9.8@SHA256:' + 'b'.repeat(64)} | ${'linux'} | ${'composer 2.9.8 sha256:' + 'b'.repeat(64)}
${'composer:2.9.8@sha384:' + 'b'.repeat(96)} | ${'linux'} | ${'add_log "$cross" "composer" "Unsupported checksum algorithm sha384, expected sha256 or sha512"'}
${'composer:2.9.8@md5:' + 'b'.repeat(32)} | ${'linux'} | ${'add_log "$cross" "composer" "Unsupported checksum algorithm md5, expected sha256 or sha512"'}
${'composer:2.9.8@sha256' + 'b'.repeat(64)} | ${'linux'} | ${'add_log "$cross" "composer" "Invalid checksum syntax, expected @sha256:<hash> or @sha512:<hash>"'}
`( `(
'checking addTools with checksum: $tools_csv, $os', 'checking addTools with checksum: $tools_csv, $os',
async ({tools_csv, os, script}) => { async ({tools_csv, os, script}) => {
+1 -1
View File
File diff suppressed because one or more lines are too long
+29 -9
View File
@@ -144,6 +144,21 @@ Function Test-MutableToolUrl() {
return ($Url -match $mutableUrlRegex) -or (($Url -match '\.phar([?#].*)?$') -and -not ($Url -match $versionLikeRegex)) return ($Url -match $mutableUrlRegex) -or (($Url -match '\.phar([?#].*)?$') -and -not ($Url -match $versionLikeRegex))
} }
# Function to verify the checksum of a file.
Function Test-ToolChecksum() {
Param(
[Parameter(Position = 0, Mandatory = $true)]
[string]
$Path,
[Parameter(Position = 1, Mandatory = $true)]
[string]
$Checksum
)
$checksum_parts = $Checksum -split ':'
$actual_checksum = (Get-FileHash -Path $Path -Algorithm $checksum_parts[0]).Hash
return $actual_checksum -eq $checksum_parts[1]
}
# Function to extract tool version. # Function to extract tool version.
Function Get-ToolVersion() { Function Get-ToolVersion() {
Param ( Param (
@@ -255,7 +270,12 @@ Function Add-Tool() {
$use_cache = -not (Test-MutableToolUrl $urls[0]) $use_cache = -not (Test-MutableToolUrl $urls[0])
$status_code = 200 $status_code = 200
if ($use_cache -and (Test-Path $cache_path -PathType Leaf)) { if ($use_cache -and (Test-Path $cache_path -PathType Leaf)) {
if($checksum -and -not(Test-ToolChecksum $cache_path $checksum)) {
Remove-Item $cache_path -Force -ErrorAction SilentlyContinue
$status_code = 'checksum_mismatch'
} else {
Copy-Item $cache_path -Destination $tool_path -Force Copy-Item $cache_path -Destination $tool_path -Force
}
} else { } else {
$backup_path = "$tool_path.bak" $backup_path = "$tool_path.bak"
if (Test-Path $tool_path) { Copy-Item $tool_path -Destination $backup_path -Force } if (Test-Path $tool_path) { Copy-Item $tool_path -Destination $backup_path -Force }
@@ -275,30 +295,30 @@ Function Add-Tool() {
} }
} }
if($status_code -eq 200 -and (Test-Path $tool_path)) { if($status_code -eq 200 -and (Test-Path $tool_path)) {
if ($use_cache) {
Copy-Item $tool_path -Destination $cache_path -Force
}
break break
} }
} }
if($status_code -eq 200 -and (Test-Path $tool_path)) {
if($checksum -and -not(Test-ToolChecksum $tool_path $checksum)) {
Remove-Item @($tool_path, $cache_path) -Force -ErrorAction SilentlyContinue
$status_code = 'checksum_mismatch'
} elseif($use_cache) {
Copy-Item $tool_path -Destination $cache_path -Force
}
}
if ($status_code -ne 200 -and (Test-Path $backup_path)) { if ($status_code -ne 200 -and (Test-Path $backup_path)) {
Copy-Item $backup_path -Destination $tool_path -Force Copy-Item $backup_path -Destination $tool_path -Force
} }
Remove-Item $backup_path -Force -ErrorAction SilentlyContinue Remove-Item $backup_path -Force -ErrorAction SilentlyContinue
} }
if($checksum -and ($status_code -eq 200) -and (Test-Path $tool_path)) { if($status_code -eq 'checksum_mismatch') {
$checksum_parts = $checksum -split ':'
$actual_checksum = (Get-FileHash -Path $tool_path -Algorithm $checksum_parts[0]).Hash
if($actual_checksum -ne $checksum_parts[1]) {
Remove-Item @($tool_path, $cache_path) -Force -ErrorAction SilentlyContinue
if($tool -eq "composer") { if($tool -eq "composer") {
$env:fail_fast = 'true' $env:fail_fast = 'true'
} }
Add-Log $cross $tool "Checksum verification failed for $tool" Add-Log $cross $tool "Checksum verification failed for $tool"
return return
} }
}
$escaped_tool = [regex]::Escape($tool) $escaped_tool = [regex]::Escape($tool)
if (((Get-ChildItem -Path $bin_dir/* | Where-Object Name -Match "^$escaped_tool(\.exe|\.phar)?$").Count -gt 0)) { if (((Get-ChildItem -Path $bin_dir/* | Where-Object Name -Match "^$escaped_tool(\.exe|\.phar)?$").Count -gt 0)) {
+13 -6
View File
@@ -227,7 +227,12 @@ add_tool() {
is_mutable_tool_url "${url[0]}" && use_cache=false is_mutable_tool_url "${url[0]}" && use_cache=false
status_code="200" status_code="200"
if [ "$use_cache" = "true" ] && [ -f "$cache_path" ]; then if [ "$use_cache" = "true" ] && [ -f "$cache_path" ]; then
if [ -n "$checksum" ] && ! verify_checksum "$cache_path" "$checksum"; then
sudo rm -f "$cache_path"
status_code="checksum_mismatch"
else
sudo cp -a "$cache_path" "$tool_path" sudo cp -a "$cache_path" "$tool_path"
fi
else else
[ -f "$tool_path" ] && sudo cp -a "$tool_path" "$tool_path.bak" [ -f "$tool_path" ] && sudo cp -a "$tool_path" "$tool_path.bak"
status_code=$(get -v -e "$tool_path" "${url[@]}") status_code=$(get -v -e "$tool_path" "${url[@]}")
@@ -236,16 +241,18 @@ add_tool() {
status_code=$(get -v -e "$tool_path" "${url[0]}") status_code=$(get -v -e "$tool_path" "${url[0]}")
fi fi
if [ "$status_code" = "200" ]; then if [ "$status_code" = "200" ]; then
[ "$use_cache" = "true" ] && sudo cp -a "$tool_path" "$cache_path" if [ -n "$checksum" ] && ! verify_checksum "$tool_path" "$checksum"; then
elif [ -f "$tool_path.bak" ]; then sudo rm -f "$tool_path" "$cache_path"
status_code="checksum_mismatch"
elif [ "$use_cache" = "true" ]; then
sudo cp -a "$tool_path" "$cache_path"
fi
fi
if [ "$status_code" != "200" ] && [ -f "$tool_path.bak" ]; then
sudo mv "$tool_path.bak" "$tool_path" sudo mv "$tool_path.bak" "$tool_path"
fi fi
sudo rm -f "$tool_path.bak" sudo rm -f "$tool_path.bak"
fi fi
if [ "$status_code" = "200" ] && [ -n "$checksum" ] && ! verify_checksum "$tool_path" "$checksum"; then
sudo rm -f "$tool_path" "$cache_path"
status_code="checksum_mismatch"
fi
if [ "$status_code" = "200" ]; then if [ "$status_code" = "200" ]; then
add_tools_helper "$tool" add_tools_helper "$tool"
tool_version=$(get_tool_version "$tool" "$ver_param") tool_version=$(get_tool_version "$tool" "$ver_param")
+7 -6
View File
@@ -130,12 +130,13 @@ verify_checksum() {
local algo="${checksum%%:*}" local algo="${checksum%%:*}"
local expected="${checksum#*:}" local expected="${checksum#*:}"
local actual= local actual=
if command -v "${algo}sum" >/dev/null; then local hash_command=
actual="$(sudo "${algo}sum" "$file_path" | cut -d' ' -f1)" if hash_command="$(command -v "${algo}sum")"; then
elif command -v shasum >/dev/null; then actual="$(sudo "$hash_command" "$file_path" | cut -d' ' -f1)"
actual="$(sudo shasum -a "${algo#sha}" "$file_path" | cut -d' ' -f1)" elif hash_command="$(command -v shasum)"; then
elif command -v openssl >/dev/null; then actual="$(sudo "$hash_command" -a "${algo#sha}" "$file_path" | cut -d' ' -f1)"
actual="$(sudo openssl dgst -"$algo" "$file_path" | awk '{print $NF}')" elif hash_command="$(command -v openssl)"; then
actual="$(sudo "$hash_command" dgst -"$algo" "$file_path" | awk '{print $NF}')"
fi fi
[ -n "$actual" ] && [ "$(echo "$actual" | tr '[:upper:]' '[:lower:]')" = "$(echo "$expected" | tr '[:upper:]' '[:lower:]')" ] [ -n "$actual" ] && [ "$(echo "$actual" | tr '[:upper:]' '[:lower:]')" = "$(echo "$expected" | tr '[:upper:]' '[:lower:]')" ]
} }
+21 -4
View File
@@ -77,7 +77,8 @@ interface ToolConfig {
/** /**
* Regex to match a checksum suffix in a tool release - tool:version@sha256:<hash> * Regex to match a checksum suffix in a tool release - tool:version@sha256:<hash>
*/ */
const checksum_suffix_regex = /@(sha256|sha512):([^@]*)$/; const checksum_suffix_regex = /@([a-z][a-z0-9-]*):([^@]*)$/i;
const checksum_like_suffix_regex = /@sha[^@]*$/i;
/** /**
* Function to parse the checksum suffix in the tool release * Function to parse the checksum suffix in the tool release
@@ -91,10 +92,24 @@ export function extractChecksum(release: string): {
} { } {
const matches = release.match(checksum_suffix_regex); const matches = release.match(checksum_suffix_regex);
if (!matches) { if (!matches) {
const checksum_like_matches = release.match(checksum_like_suffix_regex);
if (checksum_like_matches) {
return {
release: release.slice(0, -checksum_like_matches[0].length),
error:
'Invalid checksum syntax, expected @sha256:<hash> or @sha512:<hash>'
};
}
return {release}; return {release};
} }
release = release.slice(0, -matches[0].length); release = release.slice(0, -matches[0].length);
const algo = matches[1]; const algo = matches[1].toLowerCase();
if (!['sha256', 'sha512'].includes(algo)) {
return {
release,
error: `Unsupported checksum algorithm ${algo}, expected sha256 or sha512`
};
}
const hash = matches[2].toLowerCase(); const hash = matches[2].toLowerCase();
const hash_length = algo === 'sha256' ? 64 : 128; const hash_length = algo === 'sha256' ? 64 : 128;
if (!new RegExp(`^[a-f0-9]{${hash_length}}$`).test(hash)) { if (!new RegExp(`^[a-f0-9]{${hash_length}}$`).test(hash)) {
@@ -294,10 +309,12 @@ export async function filterList(tools_list: string[]): Promise<string[]> {
const regex_valid = const regex_valid =
/^composer:?($|preview$|snapshot$|v?\d+(\.\d+)?$|v?\d+\.\d+\.\d+[\w-]*$)/; /^composer:?($|preview$|snapshot$|v?\d+(\.\d+)?$|v?\d+\.\d+\.\d+[\w-]*$)/;
const matches: string[] = tools_list.filter(tool => const matches: string[] = tools_list.filter(tool =>
regex_valid.test(tool.replace(checksum_suffix_regex, '')) regex_valid.test(extractChecksum(tool).release)
); );
let composer = 'composer'; let composer = 'composer';
tools_list = tools_list.filter(tool => !regex_any.test(tool)); tools_list = tools_list.filter(
tool => !regex_any.test(extractChecksum(tool).release)
);
switch (true) { switch (true) {
case matches[0] == undefined: case matches[0] == undefined:
break; break;