mirror of
https://github.com/actions/setup-java.git
synced 2026-06-17 00:45:55 +00:00
Address PR review: RFC-compliant Link parsing, SSRF validation, centralized constant
- Make getNextPageUrlFromLinkHeader RFC 8288 compliant by splitting link-values and checking for rel=next anywhere in the parameters, not just as the first parameter after the semicolon. - Add validatePaginationUrl utility to reject pagination URLs that point to unexpected origins (SSRF mitigation). - Centralize MAX_PAGINATION_PAGES in util.ts instead of duplicating across Adopt, Semeru, and Temurin installers. - Add tests for rel not being the first parameter, and for URL origin validation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
b06c5c8844
commit
284c50b083
@ -8,7 +8,8 @@ import {
|
||||
getVersionFromFileContent,
|
||||
isVersionSatisfies,
|
||||
isCacheFeatureAvailable,
|
||||
isGhes
|
||||
isGhes,
|
||||
validatePaginationUrl
|
||||
} from '../src/util';
|
||||
|
||||
jest.mock('@actions/cache');
|
||||
@ -100,6 +101,12 @@ describe('getNextPageUrlFromLinkHeader', () => {
|
||||
},
|
||||
'https://example.com/next?page=2'
|
||||
],
|
||||
[
|
||||
{
|
||||
link: '<https://api.adoptium.net/v3/versions?page=3>; type="application/json"; rel="next"'
|
||||
},
|
||||
'https://api.adoptium.net/v3/versions?page=3'
|
||||
],
|
||||
[{link: '<https://example.com/last?page=5>; rel="last"'}, null],
|
||||
[undefined, null]
|
||||
])('returns %s -> %s', (headers, expected) => {
|
||||
@ -107,6 +114,41 @@ describe('getNextPageUrlFromLinkHeader', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('validatePaginationUrl', () => {
|
||||
it('accepts URL with matching origin', () => {
|
||||
expect(
|
||||
validatePaginationUrl(
|
||||
'https://api.adoptium.net/v3/assets?page=2',
|
||||
'https://api.adoptium.net'
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects URL with different host', () => {
|
||||
expect(
|
||||
validatePaginationUrl(
|
||||
'https://evil.example.com/steal?data=1',
|
||||
'https://api.adoptium.net'
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects URL with different protocol', () => {
|
||||
expect(
|
||||
validatePaginationUrl(
|
||||
'http://api.adoptium.net/v3/assets?page=2',
|
||||
'https://api.adoptium.net'
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for invalid URL', () => {
|
||||
expect(
|
||||
validatePaginationUrl('not-a-url', 'https://api.adoptium.net')
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVersionFromFileContent', () => {
|
||||
describe('.sdkmanrc', () => {
|
||||
it.each([
|
||||
|
||||
@ -17,11 +17,11 @@ import {
|
||||
getNextPageUrlFromLinkHeader,
|
||||
getDownloadArchiveExtension,
|
||||
isVersionSatisfies,
|
||||
renameWinArchive
|
||||
renameWinArchive,
|
||||
MAX_PAGINATION_PAGES,
|
||||
validatePaginationUrl
|
||||
} from '../../util';
|
||||
|
||||
const MAX_PAGINATION_PAGES = 1000;
|
||||
|
||||
export enum AdoptImplementation {
|
||||
Hotspot = 'Hotspot',
|
||||
OpenJ9 = 'OpenJ9'
|
||||
@ -144,7 +144,18 @@ export class AdoptDistribution extends JavaBase {
|
||||
availableVersionsUrl
|
||||
);
|
||||
const paginationPage = response.result;
|
||||
availableVersionsUrl = getNextPageUrlFromLinkHeader(response.headers);
|
||||
const nextUrl = getNextPageUrlFromLinkHeader(response.headers);
|
||||
if (
|
||||
nextUrl &&
|
||||
!validatePaginationUrl(nextUrl, 'https://api.adoptopenjdk.net')
|
||||
) {
|
||||
core.warning(
|
||||
`Ignoring pagination link with unexpected origin: ${nextUrl}`
|
||||
);
|
||||
availableVersionsUrl = null;
|
||||
} else {
|
||||
availableVersionsUrl = nextUrl;
|
||||
}
|
||||
if (paginationPage === null || paginationPage.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
@ -10,7 +10,9 @@ import {
|
||||
getNextPageUrlFromLinkHeader,
|
||||
getDownloadArchiveExtension,
|
||||
isVersionSatisfies,
|
||||
renameWinArchive
|
||||
renameWinArchive,
|
||||
MAX_PAGINATION_PAGES,
|
||||
validatePaginationUrl
|
||||
} from '../../util';
|
||||
import * as core from '@actions/core';
|
||||
import * as tc from '@actions/tool-cache';
|
||||
@ -18,8 +20,6 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
import {ISemeruAvailableVersions} from './models';
|
||||
|
||||
const MAX_PAGINATION_PAGES = 1000;
|
||||
|
||||
const supportedArchitectures = [
|
||||
'x64',
|
||||
'x86',
|
||||
@ -174,7 +174,18 @@ export class SemeruDistribution extends JavaBase {
|
||||
availableVersionsUrl
|
||||
);
|
||||
const paginationPage = response.result;
|
||||
availableVersionsUrl = getNextPageUrlFromLinkHeader(response.headers);
|
||||
const nextUrl = getNextPageUrlFromLinkHeader(response.headers);
|
||||
if (
|
||||
nextUrl &&
|
||||
!validatePaginationUrl(nextUrl, 'https://api.adoptopenjdk.net')
|
||||
) {
|
||||
core.warning(
|
||||
`Ignoring pagination link with unexpected origin: ${nextUrl}`
|
||||
);
|
||||
availableVersionsUrl = null;
|
||||
} else {
|
||||
availableVersionsUrl = nextUrl;
|
||||
}
|
||||
if (paginationPage === null || paginationPage.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
@ -17,11 +17,11 @@ import {
|
||||
getNextPageUrlFromLinkHeader,
|
||||
getDownloadArchiveExtension,
|
||||
isVersionSatisfies,
|
||||
renameWinArchive
|
||||
renameWinArchive,
|
||||
MAX_PAGINATION_PAGES,
|
||||
validatePaginationUrl
|
||||
} from '../../util';
|
||||
|
||||
const MAX_PAGINATION_PAGES = 1000;
|
||||
|
||||
export enum TemurinImplementation {
|
||||
Hotspot = 'Hotspot'
|
||||
}
|
||||
@ -142,7 +142,18 @@ export class TemurinDistribution extends JavaBase {
|
||||
availableVersionsUrl
|
||||
);
|
||||
const paginationPage = response.result;
|
||||
availableVersionsUrl = getNextPageUrlFromLinkHeader(response.headers);
|
||||
const nextUrl = getNextPageUrlFromLinkHeader(response.headers);
|
||||
if (
|
||||
nextUrl &&
|
||||
!validatePaginationUrl(nextUrl, 'https://api.adoptium.net')
|
||||
) {
|
||||
core.warning(
|
||||
`Ignoring pagination link with unexpected origin: ${nextUrl}`
|
||||
);
|
||||
availableVersionsUrl = null;
|
||||
} else {
|
||||
availableVersionsUrl = nextUrl;
|
||||
}
|
||||
|
||||
if (paginationPage === null || paginationPage.length === 0) {
|
||||
break;
|
||||
|
||||
35
src/util.ts
35
src/util.ts
@ -201,6 +201,8 @@ export function getGitHubHttpHeaders(): OutgoingHttpHeaders {
|
||||
return headers;
|
||||
}
|
||||
|
||||
export const MAX_PAGINATION_PAGES = 1000;
|
||||
|
||||
export function getNextPageUrlFromLinkHeader(
|
||||
headers?: Record<string, string | string[] | undefined>
|
||||
): string | null {
|
||||
@ -216,11 +218,36 @@ export function getNextPageUrlFromLinkHeader(
|
||||
const normalizedLinkHeader = Array.isArray(linkHeader)
|
||||
? linkHeader.join(',')
|
||||
: linkHeader;
|
||||
const nextLinkMatch = normalizedLinkHeader.match(
|
||||
/<([^>]+)>\s*;\s*rel="?next"?/i
|
||||
);
|
||||
|
||||
return nextLinkMatch?.[1] ?? null;
|
||||
// Split into individual link-values and find the one with rel="next"
|
||||
// RFC 8288 allows rel to appear anywhere among the parameters
|
||||
const linkValues = normalizedLinkHeader.split(/,(?=\s*<)/);
|
||||
for (const linkValue of linkValues) {
|
||||
const urlMatch = linkValue.match(/<([^>]+)>/);
|
||||
if (!urlMatch) continue;
|
||||
|
||||
const params = linkValue.slice(urlMatch[0].length);
|
||||
if (/;\s*rel="?next"?/i.test(params)) {
|
||||
return urlMatch[1];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function validatePaginationUrl(
|
||||
url: string,
|
||||
allowedOrigin: string
|
||||
): boolean {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
const allowed = new URL(allowedOrigin);
|
||||
return (
|
||||
parsed.protocol === allowed.protocol && parsed.host === allowed.host
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Rename archive to add extension because after downloading
|
||||
|
||||
Loading…
Reference in New Issue
Block a user