|
| 1 | +--- |
| 2 | +name: secure-relative-url-validation |
| 3 | +description: Generate secure relative url validation code. Enforces secure generation of code validating an relative url. Invoke when writing any relative url validation related code. |
| 4 | +allowed-tools: Read Grep Glob |
| 5 | +metadata: |
| 6 | + category: security |
| 7 | +--- |
| 8 | + |
| 9 | +# Secure URL Validation Code Generation Rules |
| 10 | + |
| 11 | +Apply **all** rules below when generating or reviewing any code related to validation of an relative URL. |
| 12 | + |
| 13 | +## 1. URL validation (CRITICAL) |
| 14 | + |
| 15 | +- ALWAYS ensure that the input data is recursively URL decoded prior to be validated. A decoding iteration count threshold of 4 is used and an error must be raised if the threshold is reached. |
| 16 | +- ALWAYS ensure that the input data, once URL decoded, start with one of the following character: slash, letter, number, dash, underscore. |
| 17 | +- ALWAYS ensure that the input data is a valid URL according to the [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986). |
| 18 | +- ALWAYS ensure that URL is not a absolute URL. |
| 19 | +- ALWAYS ensure that URL never start with `//`. |
| 20 | + |
| 21 | +```java |
| 22 | +import java.net.URI; |
| 23 | +import java.net.URISyntaxException; |
| 24 | +import java.net.URLDecoder; |
| 25 | +import java.nio.charset.StandardCharsets; |
| 26 | + |
| 27 | +// BAD: No validation — user input used directly as redirect target |
| 28 | +// accepts "//evil.com", "%2F%2Fevil.com", "https://evil.com" |
| 29 | +String url = request.getParameter("url"); |
| 30 | +response.sendRedirect(url); |
| 31 | + |
| 32 | +// GOOD: All rules are applied |
| 33 | +public static String parseRelativeUrl(String input) { |
| 34 | + if (input == null || input.isEmpty()) { |
| 35 | + throw new IllegalArgumentException("URL must not be null or empty."); |
| 36 | + } |
| 37 | + |
| 38 | + // Rule: recursively URL-decode to defeat encoding bypass attempts (%2F%2F, etc.) |
| 39 | + final int MAX_DECODE_ITERATIONS = 4; |
| 40 | + String decoded = input; |
| 41 | + String previous; |
| 42 | + int iterations = 0; |
| 43 | + do { |
| 44 | + if (iterations++ >= MAX_DECODE_ITERATIONS) { |
| 45 | + throw new IllegalArgumentException("URL decoding exceeded maximum iteration threshold (" + MAX_DECODE_ITERATIONS + ")."); |
| 46 | + } |
| 47 | + previous = decoded; |
| 48 | + decoded = URLDecoder.decode(previous, StandardCharsets.UTF_8); |
| 49 | + } while (!decoded.equals(previous)); |
| 50 | + |
| 51 | + // Rule: decoded value must start with slash, letter, number, dash, or underscore |
| 52 | + if (!decoded.matches("^[/a-zA-Z0-9\\-_].*")) { |
| 53 | + throw new IllegalArgumentException("URL must start with a slash, letter, number, dash, or underscore."); |
| 54 | + } |
| 55 | + |
| 56 | + // Rule: must not start with "//" (protocol relative reference) |
| 57 | + if (decoded.startsWith("//")) { |
| 58 | + throw new IllegalArgumentException("URL must not be a protocol relative reference (must not start with '//')."); |
| 59 | + } |
| 60 | + |
| 61 | + // Rule: must be a valid URI per RFC 3986 |
| 62 | + URI uri; |
| 63 | + try { |
| 64 | + uri = new URI(decoded); |
| 65 | + } catch (URISyntaxException e) { |
| 66 | + throw new IllegalArgumentException("URL is not a valid RFC 3986 URI: " + e.getMessage(), e); |
| 67 | + } |
| 68 | + |
| 69 | + // Rule: must not be an absolute URL (i.e. must have no scheme) |
| 70 | + if (uri.isAbsolute()) { |
| 71 | + throw new IllegalArgumentException("URL must not be absolute (must not have a scheme like \"https://\")."); |
| 72 | + } |
| 73 | + |
| 74 | + return decoded; |
| 75 | +} |
| 76 | +``` |
| 77 | + |
| 78 | +## 2. Output Checklist |
| 79 | + |
| 80 | +Before finalizing generated code, verify: |
| 81 | + |
| 82 | +- [ ] The input data is recursively URL decoded prior to be validated, with a maximum of 4 decode iterations enforced and an error raised if the threshold is reached. |
| 83 | +- [ ] The decoded URL starts with a slash, letter, number, dash, or underscore. |
| 84 | +- [ ] The input data is valid according to the RFC 3986. |
| 85 | +- [ ] The URL is not an absolute one. |
| 86 | +- [ ] The URL do not start with `//`. |
| 87 | + |
| 88 | +## References |
| 89 | + |
| 90 | +- [Unvalidated Redirects and Forwards Cheat Sheet from OWASP](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html). |
| 91 | +- [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986). |
| 92 | +- [WHATWG URL Living Standard](https://url.spec.whatwg.org/). |
| 93 | +- [Absolute URLs vs. relative URLs](https://developer.mozilla.org/en-US/docs/Learn_web_development/Howto/Web_mechanics/What_is_a_URL#absolute_urls_vs._relative_urls). |
| 94 | +- [CWE Open Redirect](https://cwe.mitre.org/data/definitions/601.html). |
0 commit comments