From db92f3d58c2f05dc3b4dfc6c2b2b258feb331686 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Fri, 16 Oct 2026 10:01:00 -0400 Subject: [PATCH] Reject Maven mirrors missing selectors after URL authorities Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 59df028a-0d1b-49cb-8126-ab26408663e4 --- __tests__/auth.test.ts | 19 +++++++++++++++++++ dist/setup/81.index.js | 10 +++++----- docs/advanced-usage.md | 5 +++-- src/auth.ts | 13 ++++++++----- 4 files changed, 35 insertions(+), 12 deletions(-) diff --git a/__tests__/auth.test.ts b/__tests__/auth.test.ts index 4500de15..a18693df 100644 --- a/__tests__/auth.test.ts +++ b/__tests__/auth.test.ts @@ -913,9 +913,28 @@ describe('auth tests', () => { ]); }); + it.each([ + 'https://repo.example.com:8443', + 'https://repo.example.com:8443/maven', + 'https://[2001:db8::1]', + 'https://[2001:db8::1]/maven', + 'https://[2001:db8::1]:8443', + 'https://[2001:db8::1]:8443/maven' + ])('preserves the complete mirror URL %s with an explicit selector', url => { + expect( + auth.parseMavenMirrors([`private:${url}:external:http:*,!internal`]) + ).toEqual([{id: 'private', url, mirrorOf: 'external:http:*,!internal'}]); + }); + it.each([ 'private', 'private:https://repo.example.com', + 'private:https://repo.example.com:8443', + 'private:https://repo.example.com:8443/maven', + 'private:https://[2001:db8::1]', + 'private:https://[2001:db8::1]/maven', + 'private:https://[2001:db8::1]:8443', + 'private:https://[2001:db8::1]:8443/maven', ':https://repo.example.com:*', 'private::*', 'private:https://repo.example.com:', diff --git a/dist/setup/81.index.js b/dist/setup/81.index.js index 28c50048..1dc4058b 100644 --- a/dist/setup/81.index.js +++ b/dist/setup/81.index.js @@ -234,15 +234,15 @@ function parseMavenMirrors(entries) { } const separator = entry.indexOf(':'); const remainder = entry.slice(separator + 1).trim(); - // Consume the URL scheme before looking for the selector delimiter. Selector - // tokens may themselves contain colons (external:* and external:http:*). - const scheme = /^[A-Za-z][A-Za-z0-9+.-]*:/.exec(remainder)?.[0] ?? ''; - const fields = /^(.*?):((?:\s*!?external:(?:http:)?\*\s*|[^:,]*)(?:,(?:\s*!?external:(?:http:)?\*\s*|[^:,]*))*)$/.exec(remainder.slice(scheme.length)); + // Protect scheme/authority colons (including IPv6 and ports) before + // finding the selector, which may itself contain external:http:*. + const urlPrefix = /^[A-Za-z][A-Za-z0-9+.-]*:(?:\/\/(?:[^/?#@\s]*@)?(?:\[[^\]]+\]|[^:/?#\s]+)(?::\d+(?=[/?#:\s]|$))?)?/.exec(remainder)?.[0] ?? ''; + const fields = /^(.*?):((?:\s*!?external:(?:http:)?\*\s*|[^:,]*)(?:,(?:\s*!?external:(?:http:)?\*\s*|[^:,]*))*)$/.exec(remainder.slice(urlPrefix.length)); if (separator < 0 || !fields) { throw new Error(`Invalid mvn-mirrors entry at line ${index + 1}. Expected format: mirror-id:mirror-url:mirror-of`); } const id = entry.slice(0, separator).trim(); - const url = `${scheme}${fields[1]}`.trim(); + const url = `${urlPrefix}${fields[1]}`.trim(); const mirrorOf = fields[2].trim(); if (!id || !url || !mirrorOf) { throw new Error(`Invalid mvn-mirrors entry at line ${index + 1}. mirror-id, mirror URL, and mirror-of are required`); diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index 488d1efb..11799679 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -1099,8 +1099,9 @@ Unlike repository profiles, mirrors replace the destination Maven contacts. The Central controls described above do not redirect arbitrary POM repositories. Each nonblank line has the format `mirror-id:mirror-url:mirror-of`. IDs must be -unique within this input. URL schemes and ports, and colons in `mirrorOf` -selectors, are preserved. For example: +unique within this input. URL schemes, ports, IPv6 addresses, and colons in +`mirrorOf` selectors are preserved. An explicit `mirror-of` field is required +after the complete URL, including any port. For example: ```yaml steps: diff --git a/src/auth.ts b/src/auth.ts index c3c25112..473343d3 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -355,12 +355,15 @@ export function parseMavenMirrors(entries: string[]): MavenMirror[] { const separator = entry.indexOf(':'); const remainder = entry.slice(separator + 1).trim(); - // Consume the URL scheme before looking for the selector delimiter. Selector - // tokens may themselves contain colons (external:* and external:http:*). - const scheme = /^[A-Za-z][A-Za-z0-9+.-]*:/.exec(remainder)?.[0] ?? ''; + // Protect scheme/authority colons (including IPv6 and ports) before + // finding the selector, which may itself contain external:http:*. + const urlPrefix = + /^[A-Za-z][A-Za-z0-9+.-]*:(?:\/\/(?:[^/?#@\s]*@)?(?:\[[^\]]+\]|[^:/?#\s]+)(?::\d+(?=[/?#:\s]|$))?)?/.exec( + remainder + )?.[0] ?? ''; const fields = /^(.*?):((?:\s*!?external:(?:http:)?\*\s*|[^:,]*)(?:,(?:\s*!?external:(?:http:)?\*\s*|[^:,]*))*)$/.exec( - remainder.slice(scheme.length) + remainder.slice(urlPrefix.length) ); if (separator < 0 || !fields) { throw new Error( @@ -369,7 +372,7 @@ export function parseMavenMirrors(entries: string[]): MavenMirror[] { } const id = entry.slice(0, separator).trim(); - const url = `${scheme}${fields[1]}`.trim(); + const url = `${urlPrefix}${fields[1]}`.trim(); const mirrorOf = fields[2].trim(); if (!id || !url || !mirrorOf) { throw new Error(