Require full tool versions for checksum pinning

This commit is contained in:
Shivam Mathur
2026-09-11 20:49:43 +05:30
parent 0c6410c670
commit e6ee35895b
4 changed files with 70 additions and 20 deletions
+1 -1
View File
@@ -272,7 +272,7 @@ These tools can be set up globally using the `tools` input. It accepts a string
tools: composer:2.9.8@sha256:59b2c50e10cafa0d8efc19ede9a326d782f096c674a26baf98cf042ce23de890 tools: composer:2.9.8@sha256:59b2c50e10cafa0d8efc19ede9a326d782f096c674a26baf98cf042ce23de890
``` ```
Checksum verification is supported for tools which are downloaded as phar archives. It is not supported for tools set up using `composer` packages or custom package scripts, specifying a checksum for these tools will result in an error. For checksum pinning to be effective, pin the tool to an exact version, as mutable versions like `latest` or `major.minor` can resolve to a different release with a different checksum. Checksum verification is supported only for tools downloaded as phar archives with a full version, such as `tool:1.2.3` or `tool:1.2.3-beta1`. Specifying a checksum with an omitted version, a variable tag such as `latest`, `stable`, `preview` or `snapshot`, or a partial version such as `2`, `2.x`, `2.9` or `2.9.x` results in an error. These versions can resolve to different releases with different checksums. Checksum verification is not supported for tools set up using `composer` packages or custom package scripts; specifying a checksum for these tools also results in an error.
- The latest stable version of `composer` is set up by default. You can set up the required `composer` version by specifying the major version `v1` or `v2`, or the version in `major.minor` or `semver` format. Additionally, for composer `snapshot` and `preview` can also be specified to set up the respective releases. - The latest stable version of `composer` is set up by default. You can set up the required `composer` version by specifying the major version `v1` or `v2`, or the version in `major.minor` or `semver` format. Additionally, for composer `snapshot` and `preview` can also be specified to set up the respective releases.
+40 -1
View File
@@ -228,6 +228,7 @@ describe('Tools tests', () => {
${'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}
${'tool:v1.2.3-beta.1+build.2@sha256:' + 'a'.repeat(64)} | ${'tool:v1.2.3-beta.1+build.2'} | ${'sha256:' + 'a'.repeat(64)} | ${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'}
@@ -836,7 +837,7 @@ describe('Tools tests', () => {
${'cs2pr:1.2.3@sha256:' + 'd'.repeat(64)} | ${'linux'} | ${'add_tool https://github.com/staabm/annotate-pull-request-from-checkstyle/releases/download/1.2.3/cs2pr cs2pr "-V" sha256:' + 'd'.repeat(64)} ${'cs2pr:1.2.3@sha256:' + 'd'.repeat(64)} | ${'linux'} | ${'add_tool https://github.com/staabm/annotate-pull-request-from-checkstyle/releases/download/1.2.3/cs2pr cs2pr "-V" sha256:' + 'd'.repeat(64)}
${'phive:0.15.3@sha512:' + 'c'.repeat(128)} | ${'darwin'} | ${'add_tool https://github.com/phar-io/phive/releases/download/0.15.3/phive-0.15.3.phar phive "status" sha512:' + 'c'.repeat(128)} ${'phive:0.15.3@sha512:' + 'c'.repeat(128)} | ${'darwin'} | ${'add_tool https://github.com/phar-io/phive/releases/download/0.15.3/phive-0.15.3.phar phive "status" sha512:' + 'c'.repeat(128)}
${'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:1.2.3@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@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@sha384:' + 'b'.repeat(96)} | ${'linux'} | ${'add_log "$cross" "composer" "Unsupported checksum algorithm sha384, expected sha256 or sha512"'}
@@ -849,6 +850,44 @@ describe('Tools tests', () => {
} }
); );
describe.each(['linux', 'darwin', 'win32'])(
'Checksum version requirements on %s',
os => {
it.each([
'',
':latest',
':stable',
':preview',
':snapshot',
':2',
':2.x',
':2.9',
':2.9.x',
':^2.9.8'
])(
'rejects a checksum on a non-full version %s without an unpinned install',
async version => {
for (const tool of ['composer', 'phpunit']) {
const release = `${tool}${version}@sha256:${'a'.repeat(64)}`;
const data = await tools.getData(release, '8.4', os);
expect(data.error).toBe(
'Checksum pinning requires a full version, for example tool:1.2.3'
);
expect(data.url).toBe('');
const script = await tools.addTools(release, '8.4', os);
expect(script).toContain(data.error);
const installations = script
.split('\n')
.filter(line => /^add[-_]tool /i.test(line));
expect(
installations.some(line => new RegExp(` ${tool}( |$)`).test(line))
).toBe(false);
}
}
);
}
);
it.each` it.each`
type | tool_function | supported type | tool_function | supported
${'phar'} | ${undefined} | ${true} ${'phar'} | ${undefined} | ${true}
+1 -1
View File
File diff suppressed because one or more lines are too long
+13 -2
View File
@@ -118,6 +118,12 @@ export function extractChecksum(release: string): {
error: `Invalid ${algo} checksum, expected ${hash_length} hexadecimal characters` error: `Invalid ${algo} checksum, expected ${hash_length} hexadecimal characters`
}; };
} }
if (!/^[^:]+:v?\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/.test(release)) {
return {
release,
error: 'Checksum pinning requires a full version, for example tool:1.2.3'
};
}
return {release, checksum: `${algo}:${hash}`}; return {release, checksum: `${algo}:${hash}`};
} }
@@ -308,9 +314,13 @@ export async function filterList(tools_list: string[]): Promise<string[]> {
const regex_any = /^composer($|:.*)/; const regex_any = /^composer($|:.*)/;
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(extractChecksum(tool).release) const parsed = extractChecksum(tool);
return (
regex_valid.test(parsed.release) ||
(regex_any.test(parsed.release) && parsed.error !== undefined)
); );
});
let composer = 'composer'; let composer = 'composer';
tools_list = tools_list.filter( tools_list = tools_list.filter(
tool => !regex_any.test(extractChecksum(tool).release) tool => !regex_any.test(extractChecksum(tool).release)
@@ -721,6 +731,7 @@ export async function getData(
data.checksum = checksum_data.checksum; data.checksum = checksum_data.checksum;
data.error = checksum_data.error; data.error = checksum_data.error;
data.release = await getRelease(release, data); data.release = await getRelease(release, data);
if (data.error !== undefined) return data;
data.version = version data.version = version
? await getVersion(version, data) ? await getVersion(version, data)
: await getLatestVersion(data); : await getLatestVersion(data);