From 7a2479f08c46d55eec6e49b3c35c607c3fde9b7f Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 07:24:12 +0000 Subject: [PATCH 1/5] Test the callback URL against the specification's six rules Adds the refused, accepted and logged cases from the JavaScript builder specification's callback URL section, and removes from the accepted hosts every host the section now refuses. The new cases fail until the check follows the rules. --- .../javascriptbuilder/JavaScriptUrlTests.java | 31 +++- .../flowelements/CallbackUrlTests.java | 145 ++++++++++++++---- 2 files changed, 141 insertions(+), 35 deletions(-) diff --git a/pipeline/pipeline.javascriptbuilder/src/test/java/fiftyone/pipeline/javascriptbuilder/JavaScriptUrlTests.java b/pipeline/pipeline.javascriptbuilder/src/test/java/fiftyone/pipeline/javascriptbuilder/JavaScriptUrlTests.java index 67006a69..7e5f4cdb 100644 --- a/pipeline/pipeline.javascriptbuilder/src/test/java/fiftyone/pipeline/javascriptbuilder/JavaScriptUrlTests.java +++ b/pipeline/pipeline.javascriptbuilder/src/test/java/fiftyone/pipeline/javascriptbuilder/JavaScriptUrlTests.java @@ -34,6 +34,7 @@ import fiftyone.pipeline.jsonbuilder.flowelements.JsonBuilderElementBuilder; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; import org.slf4j.ILoggerFactory; import org.slf4j.Logger; @@ -230,6 +231,33 @@ public void Url_RefusedHost_NoCallbackAndLoggedOnce() throws Exception { errors.get(0)); } + /** + * A refused host is logged with *** in place of everything in the host + * value before its last '@', so a password is never written to the log. + * Each case has its own element, because a refusal is logged once per + * element. + */ + @ParameterizedTest(name = "[{index}]") + @CsvSource(delimiter = '|', value = { + "user:secret@example.com|\"https://***@example.com/json\"", + "a@b@example.com|\"https://***@example.com/json\"", + "u@[::1]:80x|\"https://***@[::1]:80x/json\"", + "user:pa/ss@example.com|\"https://***@example.com/json\"", + "user:pa?ss@example.com|\"https://***@example.com/json\"", + "exa mple.com/a@b|\"https://***@b/json\"", + "example.123|\"https://example.123/json\""}) + public void Url_RefusedHost_LoggedWithoutUserInformation( + String host, String logged) throws Exception { + renderWithHosts(host); + + List errors = builderErrors(); + assertEquals(1, errors.size(), String.join("\n", errors)); + assertTrue(errors.get(0).contains(logged), errors.get(0)); + for (String secret : new String[] {"secret", "pa/", "pa?", "ss@"}) { + assertFalse(errors.get(0).contains(secret), errors.get(0)); + } + } + /** * A protocol from the request that is neither http nor https gives a * script with no callback, and is logged once like any other URL that @@ -273,7 +301,8 @@ public void Url_RefusedProtocol_NoCallbackAndLoggedOnce() throws Exception { * refused too, which String.equalsIgnoreCase would match to 's'. */ @ParameterizedTest - @ValueSource(strings = {"ftp", "ws", "javascript", "https ", "http\u017f"}) + @ValueSource(strings = {"ftp", "ws", "javascript", "https ", "http\u017f", + " ", "\t", "\u00a0"}) public void Protocol_ConfiguredOtherThanHttpOrHttps_IsRefused( String protocol) { JavaScriptBuilderElementBuilder builder = diff --git a/pipeline/pipeline.javascriptbuilder/src/test/java/fiftyone/pipeline/javascriptbuilder/flowelements/CallbackUrlTests.java b/pipeline/pipeline.javascriptbuilder/src/test/java/fiftyone/pipeline/javascriptbuilder/flowelements/CallbackUrlTests.java index b4be236e..26359d8f 100644 --- a/pipeline/pipeline.javascriptbuilder/src/test/java/fiftyone/pipeline/javascriptbuilder/flowelements/CallbackUrlTests.java +++ b/pipeline/pipeline.javascriptbuilder/src/test/java/fiftyone/pipeline/javascriptbuilder/flowelements/CallbackUrlTests.java @@ -109,26 +109,34 @@ static Stream acceptedHosts() { "https://-ab.com/51dpipeline/json"), Arguments.of("127.0.0.1:80", "https://127.0.0.1:80/51dpipeline/json"), + Arguments.of("example.com", + "https://example.com/51dpipeline/json"), + Arguments.of("1.2.3.4", + "https://1.2.3.4/51dpipeline/json"), + Arguments.of("1.2.3.4:8080", + "https://1.2.3.4:8080/51dpipeline/json"), + Arguments.of("1.2.3.4/path", + "https://1.2.3.4/path/51dpipeline/json"), + Arguments.of("0.0.0.0", + "https://0.0.0.0/51dpipeline/json"), + Arguments.of("101.2.3.4", + "https://101.2.3.4/51dpipeline/json"), + Arguments.of("example.co1", + "https://example.co1/51dpipeline/json"), + Arguments.of("123.example.com", + "https://123.example.com/51dpipeline/json"), Arguments.of("[::1]", "https://[::1]/51dpipeline/json"), Arguments.of("[::1]:8080", "https://[::1]:8080/51dpipeline/json"), - Arguments.of("[fe80::1%25eth0]", - "https://[fe80::1%25eth0]/51dpipeline/json"), - Arguments.of("user:pw@example.com", - "https://user:pw@example.com/51dpipeline/json"), - Arguments.of("u x@example.com", - "https://u%20x@example.com/51dpipeline/json"), - Arguments.of("u\tx@example.com", - "https://u%09x@example.com/51dpipeline/json"), Arguments.of("example.com.", "https://example.com./51dpipeline/json"), Arguments.of("example.com/path", "https://example.com/path/51dpipeline/json"), - Arguments.of("example.com?x", - "https://example.com?x/51dpipeline/json"), Arguments.of("example.com/a b", "https://example.com/a%20b/51dpipeline/json"), + Arguments.of("example.com/a\"b", + "https://example.com/a%22b/51dpipeline/json"), Arguments.of("example.com/a%zzb", "https://example.com/a%25zzb/51dpipeline/json"), Arguments.of("example.com/a%41b", @@ -183,7 +191,54 @@ static Stream refusedHosts() { "a\nb.com", " example.com", "example.com ", - "example.com:8080');alert(1);//"); + "example.com:8080');alert(1);//", + // User information, or an @ anywhere in the host value. + "user@example.com", + "user:pw@example.com", + "@example.com", + ":@example.com", + "user@example.com:8080", + "user@[::1]", + "user@example.com/path", + "u%41@example.com", + "u x@example.com", + "u\tx@example.com", + "u\"x@example.com", + "u'x@example.com", + "u\\x@example.com", + "u\nx@example.com", + "u\u0001x@example.com", + "example.com/a@b", + "user:12/ss@example.com", + "user:pa/ss@example.com", + "user:pa?ss@example.com", + "exa mple.com/a@b", + // A query or fragment. + "example.com?x", + "example.com#x", + "example.com?", + "example.com#", + "example.com:8080?x", + "[::1]#x", + "example.com/path?x", + "example.com/path#x", + "example.com?');alert(1);//", + "example.com#');alert(1);//", + // A name whose last label is a number must be an IPv4 address. + "example.123", + "example.0x1f", + "example.0X", + "1.2.3.999", + "1.2.3", + "127.1", + "0x7f.0.0.1", + "01.2.3.4", + "example.123.", + "1.2.3.4.", + "1.2.3.4.5", + "1", + "300.1.1.1", + "0x7f.1"); } @ParameterizedTest(name = "[{index}]") @@ -195,6 +250,13 @@ public void CallbackUrl_RefusesHost(String host) { static Stream refusedZonesAndTextAfterTheBracket() { return Stream.of( "[fe80::1%25]", + "[fe80::1%25eth0]", + "[fe80::1%25eth0]:8080", + "[FE80::1%25ETH0]", + "[fe80::1%251]", + "[::1%25lo]", + "[fe80::1%]", + "[fe80::1%25eth0]/path", "[fe80::1%25'+alert(1)+']", "[fe80::1%eth0]", "[fe80::1%25eth%0]", @@ -205,10 +267,8 @@ static Stream refusedZonesAndTextAfterTheBracket() { } /** - * Inside the brackets only an IPv6 address, optionally followed by '%25' - * and a zone of unreserved characters, and after them only a port. The - * zone check is what keeps a quote out of the script for a bracketed - * host. + * Inside the brackets only an IPv6 address, so a zone identifier is + * refused, and after them only a port. */ @ParameterizedTest(name = "[{index}]") @MethodSource("refusedZonesAndTextAfterTheBracket") @@ -216,14 +276,6 @@ public void CallbackUrl_RefusesZoneOrTextAfterTheBracket(String host) { assertInvalid(url("https", host), "host " + host); } - @Test - public void CallbackUrl_AcceptsZoneFollowedByPort() { - assertValid( - "https://[fe80::1%25eth0]:8080/51dpipeline/json", - url("https", "[fe80::1%25eth0]:8080"), - "host [fe80::1%25eth0]:8080"); - } - /** * A port goes by its value, so leading zeros are allowed, and a port too * long to read is refused rather than read as small enough. @@ -248,17 +300,14 @@ public void CallbackUrl_ChecksThePortByItsValue() { } /** - * Each character rule 8 lists, in the user information, after the host - * and in the endpoint, and a '%' without two hexadecimal digits after - * it, even at the end. + * Each character rule 8 lists, after the host and in the endpoint, and a + * '%' without two hexadecimal digits after it, even at the end. */ @Test public void CallbackUrl_EncodesTheCharactersRule8Lists() { assertValid( - "https://u%22%5E%60%7B%7C%7D@example.com/%22%5E%60%7B%7C%7D" + - "/j%22%5E%60%7B%7C%7D", - CallbackUrl.of( - "https", "u\"^`{|}@example.com/\"^`{|}", "/j\"^`{|}"), + "https://example.com/%22%5E%60%7B%7C%7D/j%22%5E%60%7B%7C%7D", + CallbackUrl.of("https", "example.com/\"^`{|}", "/j\"^`{|}"), "rule 8 characters"); assertValid( "https://example.com/a%25/51dpipeline/json", @@ -278,6 +327,37 @@ public void CallbackUrl_EncodesTheCharactersRule8Lists() { "endpoint /json%"); } + static Stream refusedUrlsForTheLog() { + return Stream.of( + Arguments.of("user:secret@example.com", + "https://***@example.com/51dpipeline/json"), + Arguments.of("a@b@example.com", + "https://***@example.com/51dpipeline/json"), + Arguments.of("u@[::1]:80x", + "https://***@[::1]:80x/51dpipeline/json"), + Arguments.of("user:pa/ss@example.com", + "https://***@example.com/51dpipeline/json"), + Arguments.of("user:pa?ss@example.com", + "https://***@example.com/51dpipeline/json"), + Arguments.of("exa mple.com/a@b", + "https://***@b/51dpipeline/json"), + Arguments.of("@example.com", + "https://***@example.com/51dpipeline/json")); + } + + /** + * The URL kept for the log shows *** in place of everything in the host + * value before its last '@'. + */ + @ParameterizedTest(name = "[{index}]") + @MethodSource("refusedUrlsForTheLog") + public void CallbackUrl_RefusedUrlForTheLogHasNoUserInformation( + String host, String expected) { + CallbackUrl url = url("https", host); + assertInvalid(url, "host " + host); + assertEquals(expected, url.url, "host " + host); + } + @Test public void CallbackUrl_InvalidUrlIsTheOneBuilt() { CallbackUrl url = url("https", "exa mple.com"); @@ -317,10 +397,7 @@ public void CallbackUrl_RefusesScheme(String protocol) { static Stream hostsThatCouldEndTheString() { return Stream.of( "example.com/');alert(1);//", - "example.com?');alert(1);//", - "example.com#');alert(1);//", - "u'x@example.com", - "u\\x@example.com", + "example.com/a\"b", "example.com/a\\b", "example.com/"); } From c4f9bbb0308d589e46bb5b2f38c93b8b0ecc954b Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 07:28:45 +0000 Subject: [PATCH 2/5] Check the callback URL as the specification's six rules say The host value is checked as written. A host holding '?', '#' or '@' is refused, as is an IPv6 zone identifier and a name whose last label is a number but which is not an IPv4 address in RFC 3986 form. The URL kept for the log shows *** in place of everything before the host value's last '@'. --- .../flowelements/CallbackUrl.java | 118 ++++++++++-------- 1 file changed, 63 insertions(+), 55 deletions(-) diff --git a/pipeline/pipeline.javascriptbuilder/src/main/java/fiftyone/pipeline/javascriptbuilder/flowelements/CallbackUrl.java b/pipeline/pipeline.javascriptbuilder/src/main/java/fiftyone/pipeline/javascriptbuilder/flowelements/CallbackUrl.java index c6e76aa6..f744cdb5 100644 --- a/pipeline/pipeline.javascriptbuilder/src/main/java/fiftyone/pipeline/javascriptbuilder/flowelements/CallbackUrl.java +++ b/pipeline/pipeline.javascriptbuilder/src/main/java/fiftyone/pipeline/javascriptbuilder/flowelements/CallbackUrl.java @@ -31,9 +31,11 @@ * and host usually come from the request, and the script holds the URL in a * single-quoted string, so the URL is checked before it is written into the * script. A URL that is not valid, including one whose protocol is not http - * or https, is refused. In the user information, in anything after the host - * and port, and in the endpoint, the characters a URL cannot hold or that - * could end the string or the script are percent-encoded. + * or https, is refused. In anything after the host and port, and in the + * endpoint, the characters a URL cannot hold or that could end the string or + * the script are percent-encoded. The rules are applied to the text given, + * and not to the result of parsing it, because a parser can accept hosts the + * rules refuse and rewrite others, such as 127.1. * @see Specification */ final class CallbackUrl { @@ -78,15 +80,16 @@ final class CallbackUrl { private static final String[] SCHEMES = {"http", "https"}; /** - * A label of a host name, being ASCII letters, digits, '-' and '_'. + * The characters the host value cannot hold. A '?' or '#' would make the + * endpoint part of the query or fragment, and an '@' means user + * information, which fetch refuses and XMLHttpRequest sends. */ - private static final Pattern LABEL = Pattern.compile("[A-Za-z0-9_-]+"); + private static final String NOT_IN_HOST = "?#@"; /** - * An IPv6 zone identifier, being unreserved characters as RFC 3986 - * section 2.3 defines them. + * A label of a host name, being ASCII letters, digits, '-' and '_'. */ - private static final Pattern ZONE = Pattern.compile("[A-Za-z0-9._~-]+"); + private static final Pattern LABEL = Pattern.compile("[A-Za-z0-9_-]+"); /** * A port as it follows the host, being ':' and any number of digits. @@ -107,6 +110,13 @@ final class CallbackUrl { "(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])" + "(\\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}"); + /** + * A label that the URL Standard reads as a number, being decimal digits + * or "0x" followed by hexadecimal digits. + */ + private static final Pattern NUMBER = + Pattern.compile("[0-9]+|0[xX][0-9A-Fa-f]*"); + /** * Whether the URL can be written into the script. */ @@ -114,7 +124,8 @@ final class CallbackUrl { /** * The URL to write into the script when it is valid, and otherwise the - * URL as it was built, for the log. + * URL as it was built, for the log, with *** in place of everything in + * the host value before its last '@'. */ final String url; @@ -129,8 +140,7 @@ private CallbackUrl(boolean valid, String url) { * and removed when both do. The protocol and the host are written in * lower case and the rest as given, apart from the percent-encoding. * @param protocol the protocol, http or https in any case - * @param host the host, which can start with user information and end - * with a port, a path, a query or a fragment + * @param host the host, which can end with a port and a path * @param endpoint the endpoint * @return the callback URL, or null when the protocol, host or endpoint * is not set @@ -146,14 +156,14 @@ static CallbackUrl of(String protocol, String host, String endpoint) { } else if (hostSlash && endpointSlash) { endpoint = endpoint.substring(1); } - String built = protocol + "://" + host + endpoint; - if (isScheme(protocol) == false) { - return new CallbackUrl(false, built); - } int end = authorityEnd(host); - String authority = checkedAuthority(host.substring(0, end)); + String authority = isScheme(protocol) && hasNoForbiddenCharacter(host) + ? checkedHostPort(host.substring(0, end)) + : null; if (authority == null) { - return new CallbackUrl(false, built); + return new CallbackUrl( + false, + protocol + "://" + withoutUserInformation(host) + endpoint); } return new CallbackUrl( true, @@ -161,6 +171,28 @@ static CallbackUrl of(String protocol, String host, String endpoint) { encoded(host.substring(end)) + encoded(endpoint)); } + /** + * Whether the host value holds none of '?', '#' and '@'. + */ + private static boolean hasNoForbiddenCharacter(String host) { + for (int i = 0; i < host.length(); i++) { + if (NOT_IN_HOST.indexOf(host.charAt(i)) >= 0) { + return false; + } + } + return true; + } + + /** + * The host value with everything before its last '@' replaced by ***. + * The last '@' of the whole value is used, and not that of the + * authority, because a password can hold a '/', '?' or '#'. + */ + private static String withoutUserInformation(String host) { + int at = host.lastIndexOf('@'); + return at < 0 ? host : "***" + host.substring(at); + } + /** * Whether the protocol is http or https, ignoring the case of its ASCII * letters only. String.equalsIgnoreCase also matches a letter outside @@ -208,41 +240,20 @@ static boolean isBlank(String text) { } /** - * Where the authority ends, which is at the first '/', '?' or '#' of the - * host, or at its end. + * Where the authority ends, which is at the first '/' of the host, or at + * its end. */ private static int authorityEnd(String host) { - for (int i = 0; i < host.length(); i++) { - char c = host.charAt(i); - if (c == '/' || c == '?' || c == '#') { - return i; - } - } - return host.length(); - } - - /** - * The user information, host and port as they are written into the URL, - * or null when they are not valid. The user information ends at the - * first '@', so a second '@' is part of the host and refused. - */ - private static String checkedAuthority(String authority) { - int at = authority.indexOf('@'); - if (at < 0) { - return checkedHostPort(authority); - } - String hostPort = checkedHostPort(authority.substring(at + 1)); - return hostPort == null - ? null - : encoded(authority.substring(0, at)) + "@" + hostPort; + int end = host.indexOf('/'); + return end < 0 ? host.length() : end; } /** * The host and optional port in lower case, or null when they are not - * valid. The host is an IPv6 address in brackets with an optional zone - * identifier, or a name of labels separated by dots with a dot allowed - * at the end. The port is ':' followed by digits up to 65535, or by - * nothing. + * valid. The host is an IPv6 address in brackets, or a name of labels + * separated by dots with a dot allowed at the end, which must be an IPv4 + * address when its last label is a number. The port is ':' followed by + * digits up to 65535, or by nothing. */ private static String checkedHostPort(String hostPort) { String host; @@ -253,14 +264,6 @@ private static String checkedHostPort(String hostPort) { return null; } String address = hostPort.substring(1, close); - int zone = address.indexOf("%25"); - if (zone >= 0) { - if (ZONE.matcher(address.substring(zone + 3)).matches() == - false) { - return null; - } - address = address.substring(0, zone); - } if (isIpv6(address) == false) { return null; } @@ -274,11 +277,16 @@ private static String checkedHostPort(String hostPort) { String name = host.endsWith(".") ? host.substring(0, host.length() - 1) : host; - for (String label : name.split("\\.", -1)) { + String[] labels = name.split("\\.", -1); + for (String label : labels) { if (LABEL.matcher(label).matches() == false) { return null; } } + if (NUMBER.matcher(labels[labels.length - 1]).matches() && + IPV4.matcher(host).matches() == false) { + return null; + } } if (port.isEmpty() == false && isPort(port) == false) { return null; From 6c9a29c79bbaecd0ecdc58f7719a941a96207a90 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 08:17:16 +0000 Subject: [PATCH 3/5] Test that a port that is present but empty is refused Adds example.com:, example.com:/path, [::1]:, https://example.com and http://example.com to the refused hosts, and the log case for the last protocol-in-host mistake, and removes example.com: from the accepted hosts. --- .../pipeline/javascriptbuilder/JavaScriptUrlTests.java | 3 ++- .../javascriptbuilder/flowelements/CallbackUrlTests.java | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/pipeline/pipeline.javascriptbuilder/src/test/java/fiftyone/pipeline/javascriptbuilder/JavaScriptUrlTests.java b/pipeline/pipeline.javascriptbuilder/src/test/java/fiftyone/pipeline/javascriptbuilder/JavaScriptUrlTests.java index 7e5f4cdb..fc8cb2f6 100644 --- a/pipeline/pipeline.javascriptbuilder/src/test/java/fiftyone/pipeline/javascriptbuilder/JavaScriptUrlTests.java +++ b/pipeline/pipeline.javascriptbuilder/src/test/java/fiftyone/pipeline/javascriptbuilder/JavaScriptUrlTests.java @@ -245,7 +245,8 @@ public void Url_RefusedHost_NoCallbackAndLoggedOnce() throws Exception { "user:pa/ss@example.com|\"https://***@example.com/json\"", "user:pa?ss@example.com|\"https://***@example.com/json\"", "exa mple.com/a@b|\"https://***@b/json\"", - "example.123|\"https://example.123/json\""}) + "example.123|\"https://example.123/json\"", + "https://example.com|\"https://https://example.com/json\""}) public void Url_RefusedHost_LoggedWithoutUserInformation( String host, String logged) throws Exception { renderWithHosts(host); diff --git a/pipeline/pipeline.javascriptbuilder/src/test/java/fiftyone/pipeline/javascriptbuilder/flowelements/CallbackUrlTests.java b/pipeline/pipeline.javascriptbuilder/src/test/java/fiftyone/pipeline/javascriptbuilder/flowelements/CallbackUrlTests.java index 26359d8f..0792bc72 100644 --- a/pipeline/pipeline.javascriptbuilder/src/test/java/fiftyone/pipeline/javascriptbuilder/flowelements/CallbackUrlTests.java +++ b/pipeline/pipeline.javascriptbuilder/src/test/java/fiftyone/pipeline/javascriptbuilder/flowelements/CallbackUrlTests.java @@ -99,8 +99,6 @@ static Stream acceptedHosts() { "https://example.com/51dpipeline/json"), Arguments.of("example.com:8080", "https://example.com:8080/51dpipeline/json"), - Arguments.of("example.com:", - "https://example.com:/51dpipeline/json"), Arguments.of("example.com:65535", "https://example.com:65535/51dpipeline/json"), Arguments.of("a_b.com", @@ -219,6 +217,12 @@ static Stream refusedHosts() { "example.com?", "example.com#", "example.com:8080?x", + // A port that is present but empty. + "example.com:", + "example.com:/path", + "[::1]:", + "https://example.com", + "http://example.com", "[::1]#x", "example.com/path?x", "example.com/path#x", From ae485dc5c6c1f9d81b8ec2250c71d6ba2b8e7a66 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 08:18:04 +0000 Subject: [PATCH 4/5] Refuse a port that is present but empty A host such as https://example.com otherwise reads as the host https with an empty port, and gives https://https://example.com/... with nothing logged. RFC 3986 section 3.2.3 allows an empty port in the syntax, and section 6.2.3 notes that a URI with an empty port is equivalent to one with none, so the host is written without the colon. --- .../javascriptbuilder/flowelements/CallbackUrl.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pipeline/pipeline.javascriptbuilder/src/main/java/fiftyone/pipeline/javascriptbuilder/flowelements/CallbackUrl.java b/pipeline/pipeline.javascriptbuilder/src/main/java/fiftyone/pipeline/javascriptbuilder/flowelements/CallbackUrl.java index f744cdb5..1929152c 100644 --- a/pipeline/pipeline.javascriptbuilder/src/main/java/fiftyone/pipeline/javascriptbuilder/flowelements/CallbackUrl.java +++ b/pipeline/pipeline.javascriptbuilder/src/main/java/fiftyone/pipeline/javascriptbuilder/flowelements/CallbackUrl.java @@ -92,9 +92,9 @@ final class CallbackUrl { private static final Pattern LABEL = Pattern.compile("[A-Za-z0-9_-]+"); /** - * A port as it follows the host, being ':' and any number of digits. + * A port as it follows the host, being ':' and at least one digit. */ - private static final Pattern PORT = Pattern.compile(":[0-9]*"); + private static final Pattern PORT = Pattern.compile(":[0-9]+"); /** * One group of an IPv6 address. @@ -253,7 +253,7 @@ private static int authorityEnd(String host) { * valid. The host is an IPv6 address in brackets, or a name of labels * separated by dots with a dot allowed at the end, which must be an IPv4 * address when its last label is a number. The port is ':' followed by - * digits up to 65535, or by nothing. + * digits up to 65535. */ private static String checkedHostPort(String hostPort) { String host; @@ -296,7 +296,7 @@ private static String checkedHostPort(String hostPort) { /** * Whether the text is ':' followed by digits whose value is no more than - * 65535, or by nothing. + * 65535. */ private static boolean isPort(String text) { if (PORT.matcher(text).matches() == false) { From b20b4e22a81234ebcc1ef389127cf882cd4cc89d Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 12:48:16 +0000 Subject: [PATCH 5/5] Test hosts that end in a line break and more IPv6 forms A trailing line break after a host, a port or a bracketed address is refused, and 1:2:3:4:5:6:7:8 and ::1.2.3.4 are accepted whilst 1::2::3 and 12345::1 are refused. --- .../flowelements/CallbackUrlTests.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pipeline/pipeline.javascriptbuilder/src/test/java/fiftyone/pipeline/javascriptbuilder/flowelements/CallbackUrlTests.java b/pipeline/pipeline.javascriptbuilder/src/test/java/fiftyone/pipeline/javascriptbuilder/flowelements/CallbackUrlTests.java index 0792bc72..12b7e3c9 100644 --- a/pipeline/pipeline.javascriptbuilder/src/test/java/fiftyone/pipeline/javascriptbuilder/flowelements/CallbackUrlTests.java +++ b/pipeline/pipeline.javascriptbuilder/src/test/java/fiftyone/pipeline/javascriptbuilder/flowelements/CallbackUrlTests.java @@ -127,6 +127,10 @@ static Stream acceptedHosts() { "https://[::1]/51dpipeline/json"), Arguments.of("[::1]:8080", "https://[::1]:8080/51dpipeline/json"), + Arguments.of("[1:2:3:4:5:6:7:8]", + "https://[1:2:3:4:5:6:7:8]/51dpipeline/json"), + Arguments.of("[::1.2.3.4]", + "https://[::1.2.3.4]/51dpipeline/json"), Arguments.of("example.com.", "https://example.com./51dpipeline/json"), Arguments.of("example.com/path", @@ -189,6 +193,9 @@ static Stream refusedHosts() { "a\nb.com", " example.com", "example.com ", + "example.com\n", + "example.com:80\n", + "[::1]\n", "example.com:8080');alert(1);//", // User information, or an @ anywhere in the host value. "user@example.com", @@ -267,7 +274,9 @@ static Stream refusedZonesAndTextAfterTheBracket() { "[::1]x", "[::1]80", "[::1]:80x", - "[::1]]"); + "[::1]]", + "[1::2::3]", + "[12345::1]"); } /**