spring-projects/spring-security · error · ServerExchangeRejectedException

The request was rejected because the URL was not normalized

Error message

The request was rejected because the URL was not normalized

What it means

StrictServerWebExchangeFirewall (WebFlux equivalent of StrictHttpFirewall) checks that the request URL is normalized — no directory traversal segments like /../, double slashes, or dot segments. If isNormalized(URI) fails, it throws ServerExchangeRejectedException and refuses the exchange. This blocks path-traversal and URL-obfuscation attacks at the edge of the filter chain.

Solutions

  1. Find the offending URL in the exception/access logs and fix the client to send normalized paths.
  2. Normalize at the proxy: configure nginx/Apache to merge slashes and resolve dot segments before forwarding.
  3. Re-check any recent proxy or gateway config changes that may double-encode paths (e.g. proxy_pass without proper decoding).
  4. As a last resort relax specific checks (setAllowUrlEncodedDoubleSlash / setStrictHttpFirewall equivalents on the ServerWebExchange firewall bean), understanding the security tradeoff.

Example fix

// before
// client calls GET /api//v1/../v1/users
// after
// client calls GET /api/v1/users
// and/or configure the proxy to normalize:
// nginx: merge_slashes on; before proxy_pass;
Defensive patterns

Strategy: validation

Validate before calling

// Client-side normalization guard (Java)
URI uri = URI.create(rawUrl).normalize();
if (rawUrl.contains("../") || rawUrl.contains("//") || !uri.getPath().equals(new URI(uri.normalize()).getPath())) {
    throw new IllegalArgumentException("URL must be normalized: " + rawUrl);
}

Try / catch

// WebFlux error handling
@Component
class RejectedExchangeHandler implements WebExceptionHandler {
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
        if (ex instanceof ServerExchangeRejectedException) {
            exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
            return exchange.getResponse().setComplete();
        }
        return Mono.error(ex);
    }
}

Prevention

When it happens

Trigger: A request arrives whose path contains non-normalized segments: /../, /./, double slashes (except after scheme), backslashes, or encoded equivalents that Spring's UriComponentsBuilder normalization flags.

Common situations: Scanners/probing tools sending traversal-style URLs; front-end proxies rewriting paths incorrectly (e.g. double-encoding); clients appending extra slashes or dot segments; legacy clients with hand-built URLs.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10). Data as JSON: /api/errors/23a6462bae30c0d2. Report an issue: GitHub.

Appendix: source

Thrown at web/src/main/java/org/springframework/security/web/server/firewall/StrictServerWebExchangeFirewall.java:195

	}

	public Set<String> getEncodedUrlBlocklist() {
		return this.encodedUrlBlocklist;
	}

	public Set<String> getDecodedUrlBlocklist() {
		return this.decodedUrlBlocklist;
	}

	@Override
	public Mono<ServerWebExchange> getFirewalledExchange(ServerWebExchange exchange) {
		return Mono.fromCallable(() -> {
			ServerHttpRequest request = exchange.getRequest();
			rejectForbiddenHttpMethod(request);
			rejectedBlocklistedUrls(request);
			rejectedUntrustedHosts(request);
			if (!isNormalized(request)) {
				throw new ServerExchangeRejectedException(
						"The request was rejected because the URL was not normalized");
			}

			exchange.getResponse().beforeCommit(() -> Mono.fromRunnable(() -> {
				ServerHttpResponse response = exchange.getResponse();
				HttpHeaders headers = response.getHeaders();
				headers.forEach((headerName, headerValues) -> {
					for (String headerValue : headerValues) {
						validateCrlf(headerName, headerValue);
					}
				});
			}));
			return new StrictFirewallServerWebExchange(exchange);
		});
	}

	private static void validateCrlf(String name, String value) {
		Assert.isTrue(!hasCrlf(name) && !hasCrlf(value), () -> "Invalid characters (CR/LF) in header " + name);

View on GitHub (pinned to 96852e8860)