From 4a7c7466142be434513ff27abb3a830fd1d9e878 Mon Sep 17 00:00:00 2001 From: Jim Manico Date: Fri, 25 Sep 2026 22:15:37 -0700 Subject: [PATCH] fix: encode ESAPI URL components and preserve context contracts --- README.md | 11 +- compatibility/src/EsapiConsumer.java | 7 + .../main/java/org/owasp/encoder/Encode.java | 23 ++- .../main/java/org/owasp/encoder/Encoders.java | 5 +- .../java/org/owasp/encoder/EncodersTest.java | 15 ++ esapi/README.md | 58 ++++++ esapi/pom.xml | 7 + .../org/owasp/encoder/esapi/ESAPIEncoder.java | 47 +++-- .../owasp/encoder/esapi/ESAPIContextTest.java | 183 ++++++++++++++++++ .../owasp/encoder/esapi/ESAPIEncoderTest.java | 2 +- .../esapi/ESAPIInitializationProbe.java | 4 +- .../test/resources/.esapi/ESAPI.properties | 1 + .../META-INF/java-encoder-advanced.tld | 4 +- .../main/resources/META-INF/java-encoder.tld | 4 +- .../META-INF/java-encoder-advanced.tld | 4 +- .../main/resources/META-INF/java-encoder.tld | 4 +- 16 files changed, 346 insertions(+), 33 deletions(-) create mode 100644 esapi/src/test/java/org/owasp/encoder/esapi/ESAPIContextTest.java diff --git a/README.md b/README.md index 5e5dd4e..801a876 100644 --- a/README.md +++ b/README.md @@ -162,7 +162,9 @@ always encodes `%`, so an already percent-encoded URI is double-encoded. - For an entire untrusted URL, parse it with `java.net.URI`, allow-list its scheme (for example `http` and `https`, rejecting a missing scheme unless relative URLs are - intended), and then encode the whole value for the enclosing context: + intended), enforce application-specific restrictions on its destination and + path, and then encode the whole value for the enclosing context. Parsing alone + does not establish safety: ```jsp @@ -171,6 +173,12 @@ always encodes `%`, so an already percent-encoded URI is double-encoded. `forUri` is retained for compatibility in all 1.x releases. Whether 2.0 removes it is tracked in [#142](https://github.com/OWASP/owasp-java-encoder/issues/142). +The ESAPI adapter's `encodeForURL` changes separately in unreleased 1.5: it now +uses `forUriComponent`, escaping URL delimiters while retaining `%20` for spaces. +Pass raw component data, not a complete or already encoded URL. See the +[adapter migration and context contracts](esapi/README.md#url-encoding-migration-in-15-unreleased) +for differences from earlier adapter releases and ESAPI's reference form encoder. + Development ----------- @@ -193,6 +201,7 @@ News ### Unreleased - 1.5.0 Development builds use `1.5.0-SNAPSHOT`; this is not a published release. +* fix: the ESAPI adapter's `encodeForURL` now encodes individual URL components with UTF-8, including reserved delimiters and literal `+`, instead of preserving whole-URI delimiters. Spaces remain `%20`, null remains the string `"null"`, and unpaired surrogates remain `-`. See the [migration guide](esapi/README.md#url-encoding-migration-in-15-unreleased) for output changes, form-encoding differences, and the retained quoted HTML/CSS/JavaScript contracts [#100](https://github.com/OWASP/owasp-java-encoder/issues/100). * feat: all four `forJavaScript*` methods encode dollar sign (`$`) as `\x24`, backtick as `\x60`, and opening brace (`{`) as `\x7b` [#129](https://github.com/OWASP/owasp-java-encoder/issues/129). Escaping `{` prevents input after a trusted `$` from completing `${...}`. Encoded output now supports literal text in ordinary (untagged) template literals as well as single- and double-quoted strings. This changes the encoded output while preserving its decoded JavaScript string value. Tagged templates (including `String.raw`), `${...}` expression bodies, JSON, and script URLs are unsupported; each method's HTML context restrictions still apply. * fix: all four `forJavaScript*` methods escape unpaired UTF-16 surrogates as `\uXXXX`, preserving their JavaScript string values through UTF-8 serialization [#135](https://github.com/OWASP/owasp-java-encoder/issues/135), and escape DEL/C1 controls (U+007F to U+009F) as `\xNN` [#163](https://github.com/OWASP/owasp-java-encoder/issues/163). Valid surrogate pairs and other non-ASCII text remain unescaped except U+2028/U+2029. These are output-fidelity changes; NEL was already ordinary JavaScript string data. * feat: add `Encode.forJson` String/Writer methods, the `json` encoder context, and `forJson` tags and EL functions in both JSP and Jakarta tag libraries [#145](https://github.com/OWASP/owasp-java-encoder/issues/145). The caller supplies double quotes. Output uses RFC 8259 string escapes and also escapes HTML script delimiters. Java `null` becomes the text `null` (the JSON string `"null"` when quoted); unpaired surrogates use Unicode escapes and may not interoperate with every JSON consumer. Prefer a serializer for complete JSON documents. The ESAPI adapter retains its existing JSON delegation and null behavior. diff --git a/compatibility/src/EsapiConsumer.java b/compatibility/src/EsapiConsumer.java index d656659..df01981 100644 --- a/compatibility/src/EsapiConsumer.java +++ b/compatibility/src/EsapiConsumer.java @@ -39,5 +39,12 @@ public static void main(String[] args) throws Exception { Checks.origin(org.owasp.encoder.esapi.ESAPIEncoder.class); org.owasp.esapi.Encoder encoder = org.owasp.encoder.esapi.ESAPIEncoder.getInstance(); Checks.encoded(encoder.encodeForHTML("A&B<")); + if (!"a%20b%2Bc%26admin%3Dtrue%2F%23".equals( + encoder.encodeForURL("a b+c&admin=true/#"))) { + throw new AssertionError("URL component delimiters were not encoded"); + } + if (!"null".equals(encoder.encodeForURL(null))) { + throw new AssertionError("URL null contract changed"); + } } } diff --git a/core/src/main/java/org/owasp/encoder/Encode.java b/core/src/main/java/org/owasp/encoder/Encode.java index 6d22484..083cbff 100644 --- a/core/src/main/java/org/owasp/encoder/Encode.java +++ b/core/src/main/java/org/owasp/encoder/Encode.java @@ -667,9 +667,10 @@ public static void forCssUrl(Writer out, String input) *
  • The single-quote character({@code '}) is not encoded.
  • * *
  • This encoding is not intended to be used standalone. The - * output should be encoded to the target context. For example: + * output still requires application-level URL validation and encoding + * for the target context. Prefer using a validated URL directly: * {@code ...}. + * href="<%=Encode.forHtmlAttribute(validatedUri.toString())%>">...}. * (Note, the single-quote character ({@code '}) is not * encoded.)
  • * @@ -694,9 +695,11 @@ public static void forCssUrl(Writer out, String input) * safe. Encode each untrusted value inserted into a URL with * {@link #forUriComponent(String)} instead. To use an entire untrusted * URL, parse it with {@link java.net.URI}, allow-list its scheme (for - * example {@code http} and {@code https}), and then encode it for the + * example {@code http} and {@code https}), enforce any other application + * restrictions, and then encode it for the * enclosing output context, for example with - * {@link #forHtmlAttribute(String)}. This method always encodes + * {@link #forHtmlAttribute(String)}. Parsing alone does not establish safety. + * This method always encodes * {@code %}, so never apply it to a URI that is already percent-encoded. * It is retained for compatibility in all 1.x releases; removal in 2.0 is * under consideration. @@ -724,7 +727,7 @@ public static void forCssUrl(Writer out, String input) /** * Performs percent-encoding for a component of a URI, such as a query - * parameter name or value, path or query-string. In particular this + * parameter name or value, path segment, or fragment. In particular this * method ensures that special characters in the component do not get * interpreted as part of another component. * @@ -745,9 +748,13 @@ public static void forCssUrl(Writer out, String input) * Encoding Notes *