Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[5.3.39] Align RouterFunctions resource handling #11

Merged
merged 2 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -18,6 +18,7 @@

import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.function.Function;

Expand All @@ -30,6 +31,7 @@
import org.springframework.util.Assert;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriUtils;
import org.springframework.web.util.pattern.PathPattern;
import org.springframework.web.util.pattern.PathPatternParser;

Expand Down Expand Up @@ -63,12 +65,15 @@ public Mono<Resource> apply(ServerRequest request) {

pathContainer = this.pattern.extractPathWithinPattern(pathContainer);
String path = processPath(pathContainer.value());
if (path.contains("%")) {
path = StringUtils.uriDecode(path, StandardCharsets.UTF_8);
if (!StringUtils.hasText(path) || isInvalidPath(path)) {
return Mono.empty();
}
if (!StringUtils.hasLength(path) || isInvalidPath(path)) {
if (isInvalidEncodedInputPath(path)) {
return Mono.empty();
}
if (!(this.location instanceof UrlResource)) {
path = UriUtils.decode(path, StandardCharsets.UTF_8);
}

try {
Resource resource = this.location.createRelative(path);
Expand All @@ -84,7 +89,46 @@ public Mono<Resource> apply(ServerRequest request) {
}
}

private String processPath(String path) {
/**
* Process the given resource path.
* <p>The default implementation replaces:
* <ul>
* <li>Backslash with forward slash.
* <li>Duplicate occurrences of slash with a single slash.
* <li>Any combination of leading slash and control characters (00-1F and 7F)
* with a single "/" or "". For example {@code " / // foo/bar"}
* becomes {@code "/foo/bar"}.
* </ul>
*/
protected String processPath(String path) {
path = StringUtils.replace(path, "\\", "/");
path = cleanDuplicateSlashes(path);
return cleanLeadingSlash(path);
}
private String cleanDuplicateSlashes(String path) {
StringBuilder sb = null;
char prev = 0;
for (int i = 0; i < path.length(); i++) {
char curr = path.charAt(i);
try {
if (curr == '/' && prev == '/') {
if (sb == null) {
sb = new StringBuilder(path.substring(0, i));
}
continue;
}
if (sb != null) {
sb.append(path.charAt(i));
}
}
finally {
prev = curr;
}
}
return (sb != null ? sb.toString() : path);
}

private String cleanLeadingSlash(String path) {
boolean slash = false;
for (int i = 0; i < path.length(); i++) {
if (path.charAt(i) == '/') {
Expand All @@ -94,8 +138,7 @@ else if (path.charAt(i) > ' ' && path.charAt(i) != 127) {
if (i == 0 || (i == 1 && slash)) {
return path;
}
path = slash ? "/" + path.substring(i) : path.substring(i);
return path;
return (slash ? "/" + path.substring(i) : path.substring(i));
}
}
return (slash ? "/" : "");
Expand All @@ -117,6 +160,31 @@ private boolean isInvalidPath(String path) {
return false;
}

/**
* Check whether the given path contains invalid escape sequences.
* @param path the path to validate
* @return {@code true} if the path is invalid, {@code false} otherwise
*/
private boolean isInvalidEncodedInputPath(String path) {
if (path.contains("%")) {
try {
// Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars
String decodedPath = URLDecoder.decode(path, StandardCharsets.UTF_8);
if (isInvalidPath(decodedPath)) {
return true;
}
decodedPath = processPath(decodedPath);
if (isInvalidPath(decodedPath)) {
return true;
}
}
catch (IllegalArgumentException ex) {
// May not be possible to decode...
}
}
return false;
}

private boolean isResourceUnderLocation(Resource resource) throws IOException {
if (resource.getClass() != this.location.getClass()) {
return false;
Expand All @@ -142,13 +210,23 @@ else if (resource instanceof ClassPathResource) {
return true;
}
locationPath = (locationPath.endsWith("/") || locationPath.isEmpty() ? locationPath : locationPath + "/");
if (!resourcePath.startsWith(locationPath)) {
return false;
}
if (resourcePath.contains("%") && StringUtils.uriDecode(resourcePath, StandardCharsets.UTF_8).contains("../")) {
return false;
return (resourcePath.startsWith(locationPath) && !isInvalidEncodedInputPath(resourcePath));
}

private boolean isInvalidEncodedResourcePath(String resourcePath) {
if (resourcePath.contains("%")) {
// Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars...
try {
String decodedPath = URLDecoder.decode(resourcePath, StandardCharsets.UTF_8);
if (decodedPath.contains("../") || decodedPath.contains("..\\")) {
return true;
}
}
catch (IllegalArgumentException ex) {
// May not be possible to decode...
}
}
return true;
return false;
}


Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -18,6 +18,7 @@

import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.function.Function;
Expand All @@ -29,6 +30,8 @@
import org.springframework.util.Assert;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.context.support.ServletContextResource;
import org.springframework.web.util.UriUtils;
import org.springframework.web.util.pattern.PathPattern;
import org.springframework.web.util.pattern.PathPatternParser;

Expand Down Expand Up @@ -62,12 +65,15 @@ public Optional<Resource> apply(ServerRequest request) {

pathContainer = this.pattern.extractPathWithinPattern(pathContainer);
String path = processPath(pathContainer.value());
if (path.contains("%")) {
path = StringUtils.uriDecode(path, StandardCharsets.UTF_8);
if (!StringUtils.hasText(path) || isInvalidPath(path)) {
return Optional.empty();
}
if (!StringUtils.hasLength(path) || isInvalidPath(path)) {
if (isInvalidEncodedInputPath(path)) {
return Optional.empty();
}
if (!(this.location instanceof UrlResource)) {
path = UriUtils.decode(path, StandardCharsets.UTF_8);
}

try {
Resource resource = this.location.createRelative(path);
Expand All @@ -83,7 +89,46 @@ public Optional<Resource> apply(ServerRequest request) {
}
}

private String processPath(String path) {
/**
* Process the given resource path.
* <p>The default implementation replaces:
* <ul>
* <li>Backslash with forward slash.
* <li>Duplicate occurrences of slash with a single slash.
* <li>Any combination of leading slash and control characters (00-1F and 7F)
* with a single "/" or "". For example {@code " / // foo/bar"}
* becomes {@code "/foo/bar"}.
* </ul>
*/
protected String processPath(String path) {
path = StringUtils.replace(path, "\\", "/");
path = cleanDuplicateSlashes(path);
return cleanLeadingSlash(path);
}
private String cleanDuplicateSlashes(String path) {
StringBuilder sb = null;
char prev = 0;
for (int i = 0; i < path.length(); i++) {
char curr = path.charAt(i);
try {
if ((curr == '/') && (prev == '/')) {
if (sb == null) {
sb = new StringBuilder(path.substring(0, i));
}
continue;
}
if (sb != null) {
sb.append(path.charAt(i));
}
}
finally {
prev = curr;
}
}
return sb != null ? sb.toString() : path;
}

private String cleanLeadingSlash(String path) {
boolean slash = false;
for (int i = 0; i < path.length(); i++) {
if (path.charAt(i) == '/') {
Expand All @@ -93,8 +138,7 @@ else if (path.charAt(i) > ' ' && path.charAt(i) != 127) {
if (i == 0 || (i == 1 && slash)) {
return path;
}
path = slash ? "/" + path.substring(i) : path.substring(i);
return path;
return (slash ? "/" + path.substring(i) : path.substring(i));
}
}
return (slash ? "/" : "");
Expand All @@ -113,6 +157,26 @@ private boolean isInvalidPath(String path) {
return path.contains("..") && StringUtils.cleanPath(path).contains("../");
}

private boolean isInvalidEncodedInputPath(String path) {
if (path.contains("%")) {
try {
// Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars
String decodedPath = URLDecoder.decode(path, StandardCharsets.UTF_8);
if (isInvalidPath(decodedPath)) {
return true;
}
decodedPath = processPath(decodedPath);
if (isInvalidPath(decodedPath)) {
return true;
}
}
catch (IllegalArgumentException ex) {
// May not be possible to decode...
}
}
return false;
}

private boolean isResourceUnderLocation(Resource resource) throws IOException {
if (resource.getClass() != this.location.getClass()) {
return false;
Expand All @@ -129,6 +193,10 @@ else if (resource instanceof ClassPathResource) {
resourcePath = ((ClassPathResource) resource).getPath();
locationPath = StringUtils.cleanPath(((ClassPathResource) this.location).getPath());
}
else if (resource instanceof ServletContextResource servletContextResource) {
resourcePath = servletContextResource.getPath();
locationPath = StringUtils.cleanPath(((ServletContextResource) this.location).getPath());
}
else {
resourcePath = resource.getURL().getPath();
locationPath = StringUtils.cleanPath(this.location.getURL().getPath());
Expand All @@ -138,13 +206,24 @@ else if (resource instanceof ClassPathResource) {
return true;
}
locationPath = (locationPath.endsWith("/") || locationPath.isEmpty() ? locationPath : locationPath + "/");
if (!resourcePath.startsWith(locationPath)) {
return false;
}
return !resourcePath.contains("%") ||
!StringUtils.uriDecode(resourcePath, StandardCharsets.UTF_8).contains("../");
return (resourcePath.startsWith(locationPath) && !isInvalidEncodedResourcePath(resourcePath));
}

private boolean isInvalidEncodedResourcePath(String resourcePath) {
if (resourcePath.contains("%")) {
// Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars...
try {
String decodedPath = URLDecoder.decode(resourcePath, StandardCharsets.UTF_8);
if (decodedPath.contains("../") || decodedPath.contains("..\\")) {
return true;
}
}
catch (IllegalArgumentException ex) {
// May not be possible to decode...
}
}
return false;
}

@Override
public String toString() {
Expand Down