diff --git a/api/src/main/java/jakarta/servlet/http/HttpServletRequest.java b/api/src/main/java/jakarta/servlet/http/HttpServletRequest.java index 0bdc775fb..de56f21ff 100644 --- a/api/src/main/java/jakarta/servlet/http/HttpServletRequest.java +++ b/api/src/main/java/jakarta/servlet/http/HttpServletRequest.java @@ -249,9 +249,14 @@ public String toString() { *

* This method returns null if there was no extra path information. * - * @return a String, decoded by the web container, specifying extra path information that comes after the - * servlet path but before the query string in the request URL; or null if the URL does not have any extra - * path information + * @return a String specifying extra path information that comes after the servlet path but before the + * query string in the request URL; or null if the URL does not have any extra path information. The path + * will be canonicalized as per section 3.5 of the specification. This method will not return any encoded characters + * unless the container is configured specifically to allow them. + * @throws IllegalArgumentException In standard configuration, this method will never throw. However, a container may be + * configured to not reject some suspicious sequences identified by 3.5.2, furthermore the container may be configured + * to allow such paths to only be accessed via safer methods like {@link #getRequestURI()} and to throw + * IllegalArgumentException if this method is called for such suspicious paths. */ public String getPathInfo(); @@ -299,8 +304,13 @@ default public PushBuilder newPushBuilder() { * {@link jakarta.servlet.ServletContext#getContextPath()} should be considered as the prime or preferred context path * of the application. * - * @return a String specifying the portion of the request URI that indicates the context of the request - * + * @return a String specifying the portion of the request URI that indicates the context of the request. + * The path will be canonicalized as per section 3.5 of the specification. This method will not return any encoded + * characters unless the container is configured specifically to allow them. + * @throws IllegalArgumentException In standard configuration, this method will never throw. However, a container may be + * configured to not reject some suspicious sequences identified by 3.5.2, furthermore the container may be configured + * to allow such paths to only be accessed via safer methods like {@link #getRequestURI()} and to throw + * IllegalArgumentException if this method is called for such suspicious paths. * @see jakarta.servlet.ServletContext#getContextPath() */ public String getContextPath(); @@ -411,15 +421,21 @@ default public PushBuilder newPushBuilder() { public StringBuffer getRequestURL(); /** - * Returns the part of this request's URL that calls the servlet. This path starts with a "/" character and includes - * either the servlet name or a path to the servlet, but does not include any extra path information or a query string. + * Returns the part of this request's URL that calls the servlet. This path starts with a "/" character and includes the + * path to the servlet, but does not include any extra path information or a query string. * *

* This method will return an empty string ("") if the servlet used to process this request was matched using the "/*" * pattern. * - * @return a String containing the name or path of the servlet being called, as specified in the request - * URL, decoded, or an empty string if the servlet used to process the request is matched using the "/*" pattern. + * @return a String containing the path of the servlet being called, as specified in the request URL, or an + * empty string if the servlet used to process the request is matched using the "/*" pattern. The path will be + * canonicalized as per section 3.5 of the specification. This method will not return any encoded characters unless the + * container is configured specifically to allow them. + * @throws IllegalArgumentException In standard configuration, this method will never throw. However, a container may be + * configured to not reject some suspicious sequences identified by 3.5.2, furthermore the container may be configured + * to allow such paths to only be accessed via safer methods like {@link #getRequestURI()} and to throw + * IllegalArgumentException if this method is called for such suspicious paths. */ public String getServletPath(); diff --git a/api/src/test/java/jakarta/servlet/http/CanonicalUriPathTest.java b/api/src/test/java/jakarta/servlet/http/CanonicalUriPathTest.java new file mode 100644 index 000000000..08f8fedf2 --- /dev/null +++ b/api/src/test/java/jakarta/servlet/http/CanonicalUriPathTest.java @@ -0,0 +1,313 @@ +package jakarta.servlet.http; + +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.ListIterator; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.stream.Stream; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +public class CanonicalUriPathTest { + + private static final Set ENCODED_DOT_SEGMENT; + static { + Set set = Collections.newSetFromMap(new TreeMap<>(String.CASE_INSENSITIVE_ORDER)); + set.add("%2e"); + set.add("%2e%2e"); + set.add("%2e."); + set.add(".%2e"); + ENCODED_DOT_SEGMENT = Collections.unmodifiableSet(set); + } + + public static String canonicalUriPath(String uriPath, Consumer rejection) { + + // The code presented here is a non-normative implementation of the algorithm + // from section 3.5 of the specification. + + if (uriPath == null) + throw new IllegalArgumentException("null path"); + + String path = uriPath; + + // Remember start/end conditions + boolean fragment = false; + boolean startsWithSlash; + boolean dotSegmentWithParam; + boolean encodedDotSegment; + boolean emptyNonLastSegmentWithParam; + boolean emptySegmentBeforeDotDot = false; + boolean decodeError = false; + + // Discard fragment. + if (path.contains("#")) { + path = path.substring(0, path.indexOf('#')); + fragment = true; + } + + // Separation of path and query. + if (path.contains("?")) + path = path.substring(0, path.indexOf('?')); + + // This needs to be checked after removal of path and query + startsWithSlash = path.startsWith("/"); + + // Split path into segments. + List segments = new ArrayList<>(Arrays.asList(path.substring(startsWithSlash ? 1 : 0).split("/", -1))); + + // Remove path parameters. + emptyNonLastSegmentWithParam = segments.stream().limit(segments.size() - 1).anyMatch(s -> s.startsWith(";")); + dotSegmentWithParam = segments.stream().anyMatch(s -> s.startsWith(".;") || s.startsWith("..;")); + segments.replaceAll(s -> (s.contains(";")) ? s.substring(0, s.indexOf(';')) : s); + + // Decode characters + encodedDotSegment = segments.stream().anyMatch(ENCODED_DOT_SEGMENT::contains); + try { + segments.replaceAll(CanonicalUriPathTest::decode); + } catch (Exception e) { + decodeError = true; + } + + // Remove Empty Segments other than the last + AtomicInteger last = new AtomicInteger(segments.size()); + segments.removeIf(s -> last.decrementAndGet() != 0 && s.length() == 0); + + // Remove dot-segments + int count = 0; + for (ListIterator s = segments.listIterator(); s.hasNext();) { + String segment = s.next(); + if (segment.equals(".")) { + s.remove(); + } else if (segment.equals("..")) { + if (count > 0) { + s.remove(); + String prev = s.previous(); + s.remove(); + count--; + emptySegmentBeforeDotDot |= prev.length() == 0; + } + } else { + count++; + } + } + + // Concatenate segments + if (segments.size() == 0) + path = "/"; + else { + StringBuilder buf = new StringBuilder(); + if (!decodeError && uriPath.toLowerCase().contains("%2f")) { + segments.replaceAll(CanonicalUriPathTest::encode); + } + segments.forEach(s -> buf.append("/").append(s)); + path = buf.toString(); + } + + // Rejecting Errors and Suspicious Sequences + if (fragment) + rejection.accept("fragment"); + if (decodeError) + rejection.accept("decode error"); + // Any path not starting with the `"/"` character + if (!startsWithSlash) + rejection.accept("must start with /"); + // Any path starting with an initial segment of `".."` + if (!segments.isEmpty() && segments.get(0).equals("..")) + rejection.accept("leading dot-dot-segment"); + // The encoded `"/"` character + if (uriPath.toLowerCase().contains("%2f")) + rejection.accept("encoded /"); + // Any `"."` or `".."` segment that had a path parameter + if (dotSegmentWithParam) + rejection.accept("dot segment with parameter"); + // Any `"."` or `".."` segment with any encoded characters + if (encodedDotSegment) + rejection.accept("encoded dot segment"); + // Any `".."` segment preceded by an empty segment + if (emptySegmentBeforeDotDot) + rejection.accept("empty segment before dot dot"); + // Any empty segment with parameters + if (emptyNonLastSegmentWithParam) + rejection.accept("empty segment with parameters"); + // The `"\"` character encoded or not. + if (path.contains("\\")) + rejection.accept("backslash character"); + // Any control characters either encoded or not. + for (char c : path.toCharArray()) { + if (c < 0x20 || c == 0x7f) { + rejection.accept("control character"); + break; + } + } + + return path; + } + + private static String decode(String segment) { + if (segment.contains("%")) { + StringBuilder buf = new StringBuilder(); + ByteArrayOutputStream utf8 = new ByteArrayOutputStream(); + for (int i = 0; i < segment.length(); i++) { + char c = segment.charAt(i); + if (c == '%') { + int b = Integer.parseInt(segment.substring(i + 1, i + 3), 16); + if (b < 0) + throw new IllegalArgumentException("negative encoding"); + utf8.write(b); + i += 2; + } else { + if (utf8.size() > 0) { + buf.append(fromUtf8(utf8.toByteArray())); + utf8.reset(); + } + buf.append(c); + } + } + if (utf8.size() > 0) { + buf.append(fromUtf8(utf8.toByteArray())); + utf8.reset(); + } + segment = buf.toString(); + } + return segment; + } + + private static String encode(String segment) { + if (segment.contains("%") || segment.contains("/")) { + segment = segment.replace("%", "%25"); + segment = segment.replace("/", "%2F"); + } + return segment; + } + + private static CharBuffer fromUtf8(byte[] bytes) { + try { + return StandardCharsets.UTF_8.newDecoder().onMalformedInput(CodingErrorAction.REPORT).decode(ByteBuffer.wrap(bytes)); + } catch (CharacterCodingException e) { + throw new IllegalArgumentException(e); + } + } + + public static Stream data() { + List data = new ArrayList<>(); + data.add(new Object[] { "foo/bar", "/foo/bar", true }); + data.add(new Object[] { "/foo/bar", "/foo/bar", false }); + data.add(new Object[] { "/foo/bar;jsessionid=1234", "/foo/bar", false }); + data.add(new Object[] { "/foo/bar/", "/foo/bar/", false }); + data.add(new Object[] { "/foo/bar/;jsessionid=1234", "/foo/bar/", false }); + data.add(new Object[] { "/foo;/bar;", "/foo/bar", false }); + data.add(new Object[] { "/foo;/bar;/;", "/foo/bar/", false }); + data.add(new Object[] { "/foo%00/bar/", "/foo\000/bar/", true }); + data.add(new Object[] { "/foo%7Fbar", "/foo\177bar", true }); + data.add(new Object[] { "/foo%2Fbar", "/foo%2Fbar", true }); + data.add(new Object[] { "/foo%2Fb%25r", "/foo%2Fb%25r", true }); + data.add(new Object[] { "/foo/b%25r", "/foo/b%r", false }); + data.add(new Object[] { "/foo\\bar", "/foo\\bar", true }); + data.add(new Object[] { "/foo%5Cbar", "/foo\\bar", true }); + data.add(new Object[] { "/foo;%2F/bar", "/foo/bar", true }); + data.add(new Object[] { "/foo/./bar", "/foo/bar", false }); + data.add(new Object[] { "/foo/././bar", "/foo/bar", false }); + data.add(new Object[] { "/./foo/bar", "/foo/bar", false }); + data.add(new Object[] { "/foo/%2e/bar", "/foo/bar", true }); + data.add(new Object[] { "/foo/.;/bar", "/foo/bar", true }); + data.add(new Object[] { "/foo/%2e;/bar", "/foo/bar", true }); + data.add(new Object[] { "/foo/.%2Fbar", "/foo/.%2Fbar", true }); + data.add(new Object[] { "/foo/.%5Cbar", "/foo/.\\bar", true }); + data.add(new Object[] { "/foo/bar/.", "/foo/bar", false }); + data.add(new Object[] { "/foo/bar/./", "/foo/bar/", false }); + data.add(new Object[] { "/foo/bar/.;", "/foo/bar", true }); + data.add(new Object[] { "/foo/bar/./;", "/foo/bar/", false }); + data.add(new Object[] { "/foo/.bar", "/foo/.bar", false }); + data.add(new Object[] { "/foo/../bar", "/bar", false }); + data.add(new Object[] { "/foo/../../bar", "/../bar", true }); + data.add(new Object[] { "/../foo/bar", "/../foo/bar", true }); + data.add(new Object[] { "/foo/%2e%2E/bar", "/bar", true }); + data.add(new Object[] { "/foo/%2e%2e/%2E%2E/bar", "/../bar", true }); + data.add(new Object[] { "/foo/./../bar", "/bar", false }); + data.add(new Object[] { "/foo/..;/bar", "/bar", true }); + data.add(new Object[] { "/foo/%2e%2E;/bar", "/bar", true }); + data.add(new Object[] { "/foo/..%2Fbar", "/foo/..%2Fbar", true }); + data.add(new Object[] { "/foo/..%5Cbar", "/foo/..\\bar", true }); + data.add(new Object[] { "/foo/bar/..", "/foo", false }); + data.add(new Object[] { "/foo/bar/../", "/foo/", false }); + data.add(new Object[] { "/foo/bar/..;", "/foo", true }); + data.add(new Object[] { "/foo/bar/../;", "/foo/", false }); + data.add(new Object[] { "/foo/..bar", "/foo/..bar", false }); + data.add(new Object[] { "/foo/.../bar", "/foo/.../bar", false }); + data.add(new Object[] { "/foo//bar", "/foo/bar", false }); + data.add(new Object[] { "//foo//bar//", "/foo/bar/", false }); + data.add(new Object[] { "/;/foo;/;/bar/;/;", "/foo/bar/", true }); + data.add(new Object[] { "/foo//../bar", "/bar", false }); + data.add(new Object[] { "/foo/;/../bar", "/bar", true }); + data.add(new Object[] { "/foo%E2%82%ACbar", "/foo€bar", false }); + data.add(new Object[] { "/foo%20bar", "/foo bar", false }); + data.add(new Object[] { "/foo%E2%82", "/foo%E2%82", true }); + data.add(new Object[] { "/foo%E2%82bar", "/foo%E2%82bar", true }); + data.add(new Object[] { "/foo%-1/bar", "/foo%-1/bar", true }); + data.add(new Object[] { "/foo%XX/bar", "/foo%XX/bar", true }); + data.add(new Object[] { "/foo%/bar", "/foo%/bar", true }); + data.add(new Object[] { "/foo/bar%0", "/foo/bar%0", true }); + data.add(new Object[] { "/good%20/bad%/%20mix%", "/good /bad%/%20mix%", true }); + data.add(new Object[] { "/foo/bar?q", "/foo/bar", false }); + data.add(new Object[] { "/foo/bar#f", "/foo/bar", true }); + data.add(new Object[] { "/foo/bar?q#f", "/foo/bar", true }); + data.add(new Object[] { "/foo/bar/?q", "/foo/bar/", false }); + data.add(new Object[] { "/foo/bar/#f", "/foo/bar/", true }); + data.add(new Object[] { "/foo/bar/?q#f", "/foo/bar/", true }); + data.add(new Object[] { "/foo/bar;?q", "/foo/bar", false }); + data.add(new Object[] { "/foo/bar;#f", "/foo/bar", true }); + data.add(new Object[] { "/foo/bar;?q#f", "/foo/bar", true }); + data.add(new Object[] { "/", "/", false }); + data.add(new Object[] { "//", "/", false }); + data.add(new Object[] { "/;/", "/", true }); + data.add(new Object[] { "/.", "/", false }); + data.add(new Object[] { "/..", "/..", true }); + data.add(new Object[] { "/./", "/", false }); + data.add(new Object[] { "/../", "/../", true }); + data.add(new Object[] { "foo/bar/", "/foo/bar/", true }); + data.add(new Object[] { "./foo/bar/", "/foo/bar/", true }); + data.add(new Object[] { "%2e/foo/bar/", "/foo/bar/", true }); + data.add(new Object[] { "../foo/bar/", "/../foo/bar/", true }); + data.add(new Object[] { ".%2e/foo/bar/", "/../foo/bar/", true }); + data.add(new Object[] { ";/foo/bar/", "/foo/bar/", true }); + data.add(new Object[] { "/#f", "/", true }); + data.add(new Object[] { "#f", "/", true }); + data.add(new Object[] { "/?q", "/", false }); + data.add(new Object[] { "?q", "/", true }); + + return data.stream().map(Arguments::of); + } + + @ParameterizedTest + @MethodSource("data") + public void testCanonicalUriPath(String path, String expected, boolean rejected) { + List rejections = new ArrayList<>(); + String canonical = canonicalUriPath(path, rejections::add); + + Assertions.assertEquals(expected, canonical); + Assertions.assertEquals(rejected, !rejections.isEmpty()); + + // print for inclusion in adoc + System.err.printf("| `%s` | `%s` | ", path, canonical); + if (!rejections.isEmpty()) { + for (int i = 0; i < rejections.size(); i++) { + System.err.print(i == 0 ? "400 " : " & "); + System.err.print(rejections.get(i)); + } + } + System.err.println(); + } +} diff --git a/spec/src/main/asciidoc/servlet-spec-body.adoc b/spec/src/main/asciidoc/servlet-spec-body.adoc index f608e8961..170a74fa8 100644 --- a/spec/src/main/asciidoc/servlet-spec-body.adoc +++ b/spec/src/main/asciidoc/servlet-spec-body.adoc @@ -1303,6 +1303,172 @@ the header value to an `int`, a `NumberFormatException` is thrown. If the `getDateHeader` method cannot translate the header to a `Date` object, an `IllegalArgumentException` is thrown. +=== Request URI Path Processing +The path portion of the URI of an HTTP identifies the resource to be processed. As URI paths may have various non-canonical forms, it is important that all containers process URI paths in the same way so that matching to security constraints and resources is identical. + +The process described here adapts and extends the URI canonicalization process described in [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986) to create a standard Servlet URI path canonicalization process that ensures that URIs can be mapped to Servlets, Filters and security constraints in an unambiguous manner. It is also intended to provide information to reverse proxy implementations so they are aware of how requests they pass to servlet containers will be processed. + +==== Obtaining the URI Path +HTTP/1.0:: The URI path is extracted from the `Request-URI` in the `Request-Line` as defined by [RFC 1945](https://datatracker.ietf.org/doc/html/rfc1945#section-5.1). URIs in `abs_path` form are the URI path. URIs in `absoluteURI` have the protocol and authority removed to convert them to `origin-form` and thus obtain the URI path. + +HTTP/1.1:: The URI path is extracted from the `request-target` as defined by [RFC 7230](https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.1). URIs in `origin-form` are the URI path. URIs in `absolute-form` have the protocol and authority removed to convert them to `origin-form` and thus obtain the URI path. URIs in `authority-form` or `asterisk-form` are outside of the scope of this specification. + +HTTP/2:: The URI path is the `:path` pseudo header as defined by [RFC 7540](https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.3) and is passed unchanged to stage 2. + +HTTP/3:: The URI path is the `:path` pseudo header as currently defined by the [draft RFC](https://datatracker.ietf.org/doc/html/draft-ietf-quic-http-34). + +Other protocols:: Containers may support other protocols. Containers should extract an appropriate URI path for the request from the protocol and pass it to stage 2. + +==== URI Path Canonicalization + +Servlet containers may implement the standard Servlet URI path canonicalization in any manner they see fit as long as the end result is identical to the end result of the process described here. Servlet containers may provide container specific configuration options to vary the standard canonicalization process. Any such variations may have security implications and both Servlet container implementors and users are advised to be sure that they understand the implications of any such container specific canonicalization options. + +. **Discard fragment.** ++ +The path is split by the first occurrence of any `"\#"` character. The `"#"` and following fragment are discarded and the path is replaced with the character sequence preceding the `"#"` character. + +. **Separation of path and query.** ++ +The URI is split by the first occurrence of any `"?"` character to path and query. The query is preserved for later handling and the following steps applied to the path. + +. **Split path into segments.** ++ +The path is split into segments using the `"/"` character as a prefix to each segment. The separating `"/"` does not form part of the resulting segments. For example, the path `"/foo/bar/"` is split into 3 segments: `"foo"`, `"bar"` and `""`. The prefix `"/"` for the fist segment is optional, but URIs without a leading `"/"` should be rejected below. + +. **Remove path parameters.** ++ +Any segment containing the `";"` character is split at the first occurrence of `";"`. The segment is replaced by the character sequence preceding the `";"`. The characters following the `";"` are considered path parameters and may be preserved by the container for later decoding and/or processing (eg `jsessionid`). + +. **Decode.** ++ +Octets that are encoded in `%nn` form are decoded in each segment. The resulting octet sequence is treated as UTF-8 and converted to a character sequence that replaces the segment. + +. **Remove Empty Segments.** ++ +Empty segments, other than the last segment, are removed. Containers may be configured to retain all empty segments. + +. **Remove dot-segments.** ++ +All segments that are exactly `"."` are removed from the segment series. Segments that are exactly `".."` and that are preceded by a non `".."` segment are removed together with the preceding segment. This normalization differs from RFC3986 in that segments with parameters may be treated as dot segments. + +. **Concatenate segments.** ++ +The segments are concatenated into a single path string with each segment preceded by the `"/"` character. If there are no segments remaining, the resulting path is `"/"`. If a segment contains the "/" or "%" characters, and the container is configured to not reject the request for containing an encoded `"/"`, then the container should re-encode those characters to the %nn form. If any characters are re-encoded, then the `"%"` must also be re-encoded. + +. **Mapping URI to context and resource.** ++ +The decoded path is used to map the request to a context and resource within the context. This form of the URI path is used for all subsequent mapping (web applications, servlet, filters and security constraints). + +. **Rejecting Suspicious Sequences.** ++ +If suspicious sequences are discovered during the prior processing steps, the request must be rejected with a 400 bad request rather than dispatched to the target servlet. If a context is matched then the error handling of the context may be used to generate the 400 response. By default, the set of suspicious sequences is defined below, but may be configured differently by a container: + + * The presence of a fragment in the URI + * Any path not starting with the `"/"` character (e.g. `path/info`) + * Any path starting with an initial segment of `".."` (e.g. `/../path/info`) + * The encoded `"/"` character (e.g. `/path%2Finfo`) + * Any `"."` or `".."` segment that had a path parameter (e.g. `/path/..;/info`) + * Any `"."` or `".."` segment with any encoded characters (e.g. `/path/%2e%2e/info`) + * If empty segments are not removed, then `".."` segment preceded by an empty segment (e.g. `/path//../info`) + * Any empty segment other than the last segment, with parameters (e.g. `/path/;param/info` ) + * The `"\"` character encoded or not. (e.g. `/path\info`) + * Any control characters either encoded or not. (e.g. `/path%00/info`) + * Any illegal hex sequences following a % character + * Any illegal UTF-8 code sequences. + +==== Example URIs +. Example URIs +|=== +| Encoded URI path | Decoded Path | Rejected + +| `foo/bar` | `/foo/bar` | 400 must start with / +| `/foo/bar` | `/foo/bar` | +| `/foo/bar;jsessionid=1234` | `/foo/bar` | +| `/foo/bar/` | `/foo/bar/` | +| `/foo/bar/;jsessionid=1234` | `/foo/bar/` | +| `/foo;/bar;` | `/foo/bar` | +| `/foo;/bar;/;` | `/foo/bar/` | +| `/foo%00/bar/` | `/foo +| `/foo%7Fbar` | `/foobar` | 400 control character +| `/foo%2Fbar` | `/foo%2Fbar` | 400 encoded / +| `/foo%2Fb%25r` | `/foo%2Fb%25r` | 400 encoded / +| `/foo/b%25r` | `/foo/b%r` | +| `/foo\bar` | `/foo\bar` | 400 backslash character +| `/foo%5Cbar` | `/foo\bar` | 400 backslash character +| `/foo;%2F/bar` | `/foo/bar` | 400 encoded / +| `/foo/./bar` | `/foo/bar` | +| `/foo/././bar` | `/foo/bar` | +| `/./foo/bar` | `/foo/bar` | +| `/foo/%2e/bar` | `/foo/bar` | 400 encoded dot segment +| `/foo/.;/bar` | `/foo/bar` | 400 dot segment with parameter +| `/foo/%2e;/bar` | `/foo/bar` | 400 encoded dot segment +| `/foo/.%2Fbar` | `/foo/.%2Fbar` | 400 encoded / +| `/foo/.%5Cbar` | `/foo/.\bar` | 400 backslash character +| `/foo/bar/.` | `/foo/bar` | +| `/foo/bar/./` | `/foo/bar/` | +| `/foo/bar/.;` | `/foo/bar` | 400 dot segment with parameter +| `/foo/bar/./;` | `/foo/bar/` | +| `/foo/.bar` | `/foo/.bar` | +| `/foo/../bar` | `/bar` | +| `/foo/../../bar` | `/../bar` | 400 leading dot-dot-segment +| `/../foo/bar` | `/../foo/bar` | 400 leading dot-dot-segment +| `/foo/%2e%2E/bar` | `/bar` | 400 encoded dot segment +| `/foo/%2e%2e/%2E%2E/bar` | `/../bar` | 400 leading dot-dot-segment & encoded dot segment +| `/foo/./../bar` | `/bar` | +| `/foo/..;/bar` | `/bar` | 400 dot segment with parameter +| `/foo/%2e%2E;/bar` | `/bar` | 400 encoded dot segment +| `/foo/..%2Fbar` | `/foo/..%2Fbar` | 400 encoded / +| `/foo/..%5Cbar` | `/foo/..\bar` | 400 backslash character +| `/foo/bar/..` | `/foo` | +| `/foo/bar/../` | `/foo/` | +| `/foo/bar/..;` | `/foo` | 400 dot segment with parameter +| `/foo/bar/../;` | `/foo/` | +| `/foo/..bar` | `/foo/..bar` | +| `/foo/.../bar` | `/foo/.../bar` | +| `/foo//bar` | `/foo/bar` | +| `//foo//bar//` | `/foo/bar/` | +| `/;/foo;/;/bar/;/;` | `/foo/bar/` | 400 empty segment with parameters +| `/foo//../bar` | `/bar` | +| `/foo/;/../bar` | `/bar` | 400 empty segment with parameters +| `/foo%E2%82%ACbar` | `/foo€bar` | +| `/foo%20bar` | `/foo bar` | +| `/foo%E2%82` | `/foo%E2%82` | 400 decode error +| `/foo%E2%82bar` | `/foo%E2%82bar` | 400 decode error +| `/foo%-1/bar` | `/foo%-1/bar` | 400 decode error +| `/foo%XX/bar` | `/foo%XX/bar` | 400 decode error +| `/foo%/bar` | `/foo%/bar` | 400 decode error +| `/foo/bar%0` | `/foo/bar%0` | 400 decode error +| `/good%20/bad%/%20mix%` | `/good /bad%/%20mix%` | 400 decode error +| `/foo/bar?q` | `/foo/bar` | +| `/foo/bar#f` | `/foo/bar` | 400 fragment +| `/foo/bar?q#f` | `/foo/bar` | 400 fragment +| `/foo/bar/?q` | `/foo/bar/` | +| `/foo/bar/#f` | `/foo/bar/` | 400 fragment +| `/foo/bar/?q#f` | `/foo/bar/` | 400 fragment +| `/foo/bar;?q` | `/foo/bar` | +| `/foo/bar;#f` | `/foo/bar` | 400 fragment +| `/foo/bar;?q#f` | `/foo/bar` | 400 fragment +| `/` | `/` | +| `//` | `/` | +| `/;/` | `/` | 400 empty segment with parameters +| `/.` | `/` | +| `/..` | `/..` | 400 leading dot-dot-segment +| `/./` | `/` | +| `/../` | `/../` | 400 leading dot-dot-segment +| `foo/bar/` | `/foo/bar/` | 400 must start with / +| `./foo/bar/` | `/foo/bar/` | 400 must start with / +| `%2e/foo/bar/` | `/foo/bar/` | 400 must start with / & encoded dot segment +| `../foo/bar/` | `/../foo/bar/` | 400 must start with / & leading dot-dot-segment +| `.%2e/foo/bar/` | `/../foo/bar/` | 400 must start with / & leading dot-dot-segment & encoded dot segment +| `;/foo/bar/` | `/foo/bar/` | 400 must start with / & empty segment with parameters +| `/#f` | `/` | 400 fragment +| `#f` | `/` | 400 fragment & must start with / +| `/?q` | `/` | +| `?q` | `/` | 400 must start with / + +|=== + + === Request Path Elements The request path that leads to a servlet @@ -6341,11 +6507,10 @@ public @interface HttpMethodConstraint { |=== |Element |Description +|value |Default -|value -|The HTTP protocol method name -| + |`emptyRoleSemantic` @@ -8381,6 +8546,9 @@ Jakarta Servlet {spec-version} specification developed under the Jakarta EE Work === Changes Since Jakarta Servlet 5.0 +link:https://github.com/eclipse-ee4j/servlet-api/issues/18[Issue 18]:: +Clarify the decoding and normalization of URI paths. + link:https://github.com/eclipse-ee4j/servlet-api/issues/175[Issue 175]:: Provide generic attribute support to cookies, including session cookies, to provide support for additional attributes such as the `SameSite` attribute.