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
This commit is contained in:
Bruno Borges
2026-10-16 10:01:00 -04:00
parent df4305dee7
commit db92f3d58c
4 changed files with 35 additions and 12 deletions

View File

@@ -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([ it.each([
'private', 'private',
'private:https://repo.example.com', '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:*', ':https://repo.example.com:*',
'private::*', 'private::*',
'private:https://repo.example.com:', 'private:https://repo.example.com:',

View File

@@ -234,15 +234,15 @@ function parseMavenMirrors(entries) {
} }
const separator = entry.indexOf(':'); const separator = entry.indexOf(':');
const remainder = entry.slice(separator + 1).trim(); const remainder = entry.slice(separator + 1).trim();
// Consume the URL scheme before looking for the selector delimiter. Selector // Protect scheme/authority colons (including IPv6 and ports) before
// tokens may themselves contain colons (external:* and external:http:*). // finding the selector, which may itself contain external:http:*.
const scheme = /^[A-Za-z][A-Za-z0-9+.-]*:/.exec(remainder)?.[0] ?? ''; 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)); const fields = /^(.*?):((?:\s*!?external:(?:http:)?\*\s*|[^:,]*)(?:,(?:\s*!?external:(?:http:)?\*\s*|[^:,]*))*)$/.exec(remainder.slice(urlPrefix.length));
if (separator < 0 || !fields) { if (separator < 0 || !fields) {
throw new Error(`Invalid mvn-mirrors entry at line ${index + 1}. Expected format: mirror-id:mirror-url:mirror-of`); 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 id = entry.slice(0, separator).trim();
const url = `${scheme}${fields[1]}`.trim(); const url = `${urlPrefix}${fields[1]}`.trim();
const mirrorOf = fields[2].trim(); const mirrorOf = fields[2].trim();
if (!id || !url || !mirrorOf) { if (!id || !url || !mirrorOf) {
throw new Error(`Invalid mvn-mirrors entry at line ${index + 1}. mirror-id, mirror URL, and mirror-of are required`); throw new Error(`Invalid mvn-mirrors entry at line ${index + 1}. mirror-id, mirror URL, and mirror-of are required`);

View File

@@ -1099,8 +1099,9 @@ Unlike repository profiles, mirrors replace the destination Maven contacts.
The Central controls described above do not redirect arbitrary POM repositories. 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 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` unique within this input. URL schemes, ports, IPv6 addresses, and colons in
selectors, are preserved. For example: `mirrorOf` selectors are preserved. An explicit `mirror-of` field is required
after the complete URL, including any port. For example:
```yaml ```yaml
steps: steps:

View File

@@ -355,12 +355,15 @@ export function parseMavenMirrors(entries: string[]): MavenMirror[] {
const separator = entry.indexOf(':'); const separator = entry.indexOf(':');
const remainder = entry.slice(separator + 1).trim(); const remainder = entry.slice(separator + 1).trim();
// Consume the URL scheme before looking for the selector delimiter. Selector // Protect scheme/authority colons (including IPv6 and ports) before
// tokens may themselves contain colons (external:* and external:http:*). // finding the selector, which may itself contain external:http:*.
const scheme = /^[A-Za-z][A-Za-z0-9+.-]*:/.exec(remainder)?.[0] ?? ''; const urlPrefix =
/^[A-Za-z][A-Za-z0-9+.-]*:(?:\/\/(?:[^/?#@\s]*@)?(?:\[[^\]]+\]|[^:/?#\s]+)(?::\d+(?=[/?#:\s]|$))?)?/.exec(
remainder
)?.[0] ?? '';
const fields = const fields =
/^(.*?):((?:\s*!?external:(?:http:)?\*\s*|[^:,]*)(?:,(?:\s*!?external:(?:http:)?\*\s*|[^:,]*))*)$/.exec( /^(.*?):((?:\s*!?external:(?:http:)?\*\s*|[^:,]*)(?:,(?:\s*!?external:(?:http:)?\*\s*|[^:,]*))*)$/.exec(
remainder.slice(scheme.length) remainder.slice(urlPrefix.length)
); );
if (separator < 0 || !fields) { if (separator < 0 || !fields) {
throw new Error( throw new Error(
@@ -369,7 +372,7 @@ export function parseMavenMirrors(entries: string[]): MavenMirror[] {
} }
const id = entry.slice(0, separator).trim(); const id = entry.slice(0, separator).trim();
const url = `${scheme}${fields[1]}`.trim(); const url = `${urlPrefix}${fields[1]}`.trim();
const mirrorOf = fields[2].trim(); const mirrorOf = fields[2].trim();
if (!id || !url || !mirrorOf) { if (!id || !url || !mirrorOf) {
throw new Error( throw new Error(