mirror of
https://github.com/actions/setup-java.git
synced 2026-06-25 05:37:42 +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,
|
getVersionFromFileContent,
|
||||||
isVersionSatisfies,
|
isVersionSatisfies,
|
||||||
isCacheFeatureAvailable,
|
isCacheFeatureAvailable,
|
||||||
isGhes
|
isGhes,
|
||||||
|
validatePaginationUrl
|
||||||
} from '../src/util';
|
} from '../src/util';
|
||||||
|
|
||||||
jest.mock('@actions/cache');
|
jest.mock('@actions/cache');
|
||||||
@ -100,6 +101,12 @@ describe('getNextPageUrlFromLinkHeader', () => {
|
|||||||
},
|
},
|
||||||
'https://example.com/next?page=2'
|
'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],
|
[{link: '<https://example.com/last?page=5>; rel="last"'}, null],
|
||||||
[undefined, null]
|
[undefined, null]
|
||||||
])('returns %s -> %s', (headers, expected) => {
|
])('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('getVersionFromFileContent', () => {
|
||||||
describe('.sdkmanrc', () => {
|
describe('.sdkmanrc', () => {
|
||||||
it.each([
|
it.each([
|
||||||
|
|||||||
@ -17,11 +17,11 @@ import {
|
|||||||
getNextPageUrlFromLinkHeader,
|
getNextPageUrlFromLinkHeader,
|
||||||
getDownloadArchiveExtension,
|
getDownloadArchiveExtension,
|
||||||
isVersionSatisfies,
|
isVersionSatisfies,
|
||||||
renameWinArchive
|
renameWinArchive,
|
||||||
|
MAX_PAGINATION_PAGES,
|
||||||
|
validatePaginationUrl
|
||||||
} from '../../util';
|
} from '../../util';
|
||||||
|
|
||||||
const MAX_PAGINATION_PAGES = 1000;
|
|
||||||
|
|
||||||
export enum AdoptImplementation {
|
export enum AdoptImplementation {
|
||||||
Hotspot = 'Hotspot',
|
Hotspot = 'Hotspot',
|
||||||
OpenJ9 = 'OpenJ9'
|
OpenJ9 = 'OpenJ9'
|
||||||
@ -144,7 +144,18 @@ export class AdoptDistribution extends JavaBase {
|
|||||||
availableVersionsUrl
|
availableVersionsUrl
|
||||||
);
|
);
|
||||||
const paginationPage = response.result;
|
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) {
|
if (paginationPage === null || paginationPage.length === 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,9 @@ import {
|
|||||||
getNextPageUrlFromLinkHeader,
|
getNextPageUrlFromLinkHeader,
|
||||||
getDownloadArchiveExtension,
|
getDownloadArchiveExtension,
|
||||||
isVersionSatisfies,
|
isVersionSatisfies,
|
||||||
renameWinArchive
|
renameWinArchive,
|
||||||
|
MAX_PAGINATION_PAGES,
|
||||||
|
validatePaginationUrl
|
||||||
} from '../../util';
|
} from '../../util';
|
||||||
import * as core from '@actions/core';
|
import * as core from '@actions/core';
|
||||||
import * as tc from '@actions/tool-cache';
|
import * as tc from '@actions/tool-cache';
|
||||||
@ -18,8 +20,6 @@ import fs from 'fs';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {ISemeruAvailableVersions} from './models';
|
import {ISemeruAvailableVersions} from './models';
|
||||||
|
|
||||||
const MAX_PAGINATION_PAGES = 1000;
|
|
||||||
|
|
||||||
const supportedArchitectures = [
|
const supportedArchitectures = [
|
||||||
'x64',
|
'x64',
|
||||||
'x86',
|
'x86',
|
||||||
@ -174,7 +174,18 @@ export class SemeruDistribution extends JavaBase {
|
|||||||
availableVersionsUrl
|
availableVersionsUrl
|
||||||
);
|
);
|
||||||
const paginationPage = response.result;
|
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) {
|
if (paginationPage === null || paginationPage.length === 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,11 +17,11 @@ import {
|
|||||||
getNextPageUrlFromLinkHeader,
|
getNextPageUrlFromLinkHeader,
|
||||||
getDownloadArchiveExtension,
|
getDownloadArchiveExtension,
|
||||||
isVersionSatisfies,
|
isVersionSatisfies,
|
||||||
renameWinArchive
|
renameWinArchive,
|
||||||
|
MAX_PAGINATION_PAGES,
|
||||||
|
validatePaginationUrl
|
||||||
} from '../../util';
|
} from '../../util';
|
||||||
|
|
||||||
const MAX_PAGINATION_PAGES = 1000;
|
|
||||||
|
|
||||||
export enum TemurinImplementation {
|
export enum TemurinImplementation {
|
||||||
Hotspot = 'Hotspot'
|
Hotspot = 'Hotspot'
|
||||||
}
|
}
|
||||||
@ -142,7 +142,18 @@ export class TemurinDistribution extends JavaBase {
|
|||||||
availableVersionsUrl
|
availableVersionsUrl
|
||||||
);
|
);
|
||||||
const paginationPage = response.result;
|
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) {
|
if (paginationPage === null || paginationPage.length === 0) {
|
||||||
break;
|
break;
|
||||||
|
|||||||
35
src/util.ts
35
src/util.ts
@ -201,6 +201,8 @@ export function getGitHubHttpHeaders(): OutgoingHttpHeaders {
|
|||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const MAX_PAGINATION_PAGES = 1000;
|
||||||
|
|
||||||
export function getNextPageUrlFromLinkHeader(
|
export function getNextPageUrlFromLinkHeader(
|
||||||
headers?: Record<string, string | string[] | undefined>
|
headers?: Record<string, string | string[] | undefined>
|
||||||
): string | null {
|
): string | null {
|
||||||
@ -216,11 +218,36 @@ export function getNextPageUrlFromLinkHeader(
|
|||||||
const normalizedLinkHeader = Array.isArray(linkHeader)
|
const normalizedLinkHeader = Array.isArray(linkHeader)
|
||||||
? linkHeader.join(',')
|
? linkHeader.join(',')
|
||||||
: linkHeader;
|
: 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
|
// Rename archive to add extension because after downloading
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user