Skip to content

Commit 9c8690a

Browse files
authored
Merge branch 'trunk' into py_exceptions
2 parents 69905ca + 406427b commit 9c8690a

File tree

33 files changed

+1564
-300
lines changed

33 files changed

+1564
-300
lines changed

.github/workflows/ci-python.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
- name: Install dependencies
2828
run: |
2929
python -m pip install --upgrade pip
30-
pip install tox==4.25.0
30+
pip install tox==4.27.0
3131
- name: Generate docs
3232
run: tox -c py/tox.ini
3333
env:
@@ -47,7 +47,7 @@ jobs:
4747
- name: Install dependencies
4848
run: |
4949
python -m pip install --upgrade pip
50-
pip install tox==4.25.0
50+
pip install tox==4.27.0
5151
- name: Run type checking
5252
run: |
5353
tox -c py/tox.ini || true

common/repositories.bzl

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,10 @@ js_library(
123123

124124
pkg_archive(
125125
name = "mac_edge",
126-
url = "https://msedge.sf.dl.delivery.mp.microsoft.com/filestreamingservice/files/07df5fb0-0542-41c3-ad8d-889adf2b954f/MicrosoftEdge-137.0.3296.83.pkg",
127-
sha256 = "bc49d876669ae029e5f1236615cbe2e26dd2588a3048c450c1ae600fef6c454b",
126+
url = "https://msedge.sf.dl.delivery.mp.microsoft.com/filestreamingservice/files/8146afbf-4969-4acb-baa7-a1b8a83745e5/MicrosoftEdge-137.0.3296.93.pkg",
127+
sha256 = "e098a79ceb0a843ff0d9331c86b27a49a6b26ba798f28d59a342ce8c2534f54b",
128128
move = {
129-
"MicrosoftEdge-137.0.3296.83.pkg/Payload/Microsoft Edge.app": "Edge.app",
129+
"MicrosoftEdge-137.0.3296.93.pkg/Payload/Microsoft Edge.app": "Edge.app",
130130
},
131131
build_file_content = """
132132
load("@aspect_rules_js//js:defs.bzl", "js_library")
@@ -143,8 +143,8 @@ js_library(
143143

144144
deb_archive(
145145
name = "linux_edge",
146-
url = "https://packages.microsoft.com/repos/edge/pool/main/m/microsoft-edge-stable/microsoft-edge-stable_137.0.3296.83-1_amd64.deb",
147-
sha256 = "c1b8a28efc73cb233d971a0cb5c40716a6580a196c1e1d213504f0760c4fa596",
146+
url = "https://packages.microsoft.com/repos/edge/pool/main/m/microsoft-edge-stable/microsoft-edge-stable_137.0.3296.93-1_amd64.deb",
147+
sha256 = "482f21e9443f79ee5995058066c982d42a392b2e24697ba8329bd566d4c83074",
148148
build_file_content = """
149149
load("@aspect_rules_js//js:defs.bzl", "js_library")
150150
package(default_visibility = ["//visibility:public"])

java/src/org/openqa/selenium/grid/commands/Hub.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import java.net.MalformedURLException;
3232
import java.net.URL;
3333
import java.util.Collections;
34+
import java.util.List;
3435
import java.util.Set;
3536
import java.util.logging.Level;
3637
import java.util.logging.Logger;
@@ -48,6 +49,8 @@
4849
import org.openqa.selenium.grid.log.LoggingOptions;
4950
import org.openqa.selenium.grid.router.ProxyWebsocketsIntoGrid;
5051
import org.openqa.selenium.grid.router.Router;
52+
import org.openqa.selenium.grid.router.httpd.BlockedRoute;
53+
import org.openqa.selenium.grid.router.httpd.BlockedRoutesFilter;
5154
import org.openqa.selenium.grid.router.httpd.RouterOptions;
5255
import org.openqa.selenium.grid.security.BasicAuthenticationFilter;
5356
import org.openqa.selenium.grid.security.Secret;
@@ -207,6 +210,13 @@ protected Handlers createHandlers(Config config) {
207210
httpHandler = httpHandler.with(new BasicAuthenticationFilter(uap.username(), uap.password()));
208211
}
209212

213+
// Apply blocked routes filter
214+
List<BlockedRoute> blockedRoutes = routerOptions.getBlockedRoutes();
215+
if (!blockedRoutes.isEmpty()) {
216+
LOG.info("Blocking " + blockedRoutes.size() + " route(s): " + blockedRoutes);
217+
httpHandler = BlockedRoutesFilter.with(httpHandler, blockedRoutes);
218+
}
219+
210220
// Allow the liveness endpoint to be reached, since k8s doesn't make it easy to authenticate
211221
// these checks
212222
httpHandler = combine(httpHandler, Route.get("/readyz").to(() -> readinessCheck));

java/src/org/openqa/selenium/grid/commands/Standalone.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import java.net.URI;
3333
import java.net.URL;
3434
import java.util.Collections;
35+
import java.util.List;
3536
import java.util.Set;
3637
import java.util.logging.Level;
3738
import java.util.logging.Logger;
@@ -53,6 +54,8 @@
5354
import org.openqa.selenium.grid.node.ProxyNodeWebsockets;
5455
import org.openqa.selenium.grid.node.config.NodeOptions;
5556
import org.openqa.selenium.grid.router.Router;
57+
import org.openqa.selenium.grid.router.httpd.BlockedRoute;
58+
import org.openqa.selenium.grid.router.httpd.BlockedRoutesFilter;
5659
import org.openqa.selenium.grid.router.httpd.RouterOptions;
5760
import org.openqa.selenium.grid.security.BasicAuthenticationFilter;
5861
import org.openqa.selenium.grid.security.Secret;
@@ -213,6 +216,13 @@ protected Handlers createHandlers(Config config) {
213216
httpHandler = httpHandler.with(new BasicAuthenticationFilter(uap.username(), uap.password()));
214217
}
215218

219+
// Apply blocked routes filter
220+
List<BlockedRoute> blockedRoutes = routerOptions.getBlockedRoutes();
221+
if (!blockedRoutes.isEmpty()) {
222+
LOG.info("Blocking " + blockedRoutes.size() + " route(s): " + blockedRoutes);
223+
httpHandler = BlockedRoutesFilter.with(httpHandler, blockedRoutes);
224+
}
225+
216226
// Allow the liveness endpoint to be reached, since k8s doesn't make it easy to authenticate
217227
// these checks
218228
httpHandler = combine(httpHandler, Route.get("/readyz").to(() -> readinessCheck));
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
// Licensed to the Software Freedom Conservancy (SFC) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The SFC licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package org.openqa.selenium.grid.router.httpd;
19+
20+
import java.net.URLDecoder;
21+
import java.nio.charset.StandardCharsets;
22+
import java.util.ArrayList;
23+
import java.util.List;
24+
25+
/** Represents a blocked route with HTTP method and path. */
26+
public class BlockedRoute {
27+
private final String method;
28+
private final String path;
29+
30+
public BlockedRoute(String method, String path) {
31+
this.method = method.toUpperCase();
32+
this.path = path;
33+
}
34+
35+
public String getMethod() {
36+
return method;
37+
}
38+
39+
public String getPath() {
40+
return path;
41+
}
42+
43+
/**
44+
* Creates a BlockedRoute from a string in the format "METHOD:path".
45+
*
46+
* @param routeStr String representation of blocked route
47+
* @return BlockedRoute instance
48+
* @throws IllegalArgumentException if the format is invalid
49+
*/
50+
public static BlockedRoute fromString(String routeStr) {
51+
if (routeStr == null || routeStr.trim().isEmpty()) {
52+
throw new IllegalArgumentException("Route string cannot be null or empty");
53+
}
54+
55+
String[] parts = routeStr.split(":", 2);
56+
if (parts.length != 2) {
57+
throw new IllegalArgumentException(
58+
"Invalid route format. Expected 'METHOD:path', got: " + routeStr);
59+
}
60+
61+
String method = parts[0].trim().toUpperCase();
62+
String path = parts[1].trim();
63+
64+
if (method.isEmpty() || path.isEmpty()) {
65+
throw new IllegalArgumentException("Method and path cannot be empty. Got: " + routeStr);
66+
}
67+
68+
return new BlockedRoute(method, path);
69+
}
70+
71+
/**
72+
* Checks if the given HTTP method and request path match this blocked route.
73+
*
74+
* @param requestMethod HTTP method of the request
75+
* @param requestPath Path of the request
76+
* @return true if the route should be blocked
77+
*/
78+
public boolean matches(String requestMethod, String requestPath) {
79+
if (!method.equals(requestMethod.toUpperCase())) {
80+
return false;
81+
}
82+
83+
// Use safe string-based path matching instead of regex to prevent ReDoS attacks
84+
return matchesPathPattern(path, requestPath);
85+
}
86+
87+
/**
88+
* Safely matches a path pattern against a request path without using regex. Handles path
89+
* parameters like {session-id} by treating them as wildcards. Both paths are normalized to
90+
* prevent path traversal attacks.
91+
*
92+
* @param pattern The path pattern to match against
93+
* @param requestPath The actual request path
94+
* @return true if the paths match
95+
*/
96+
private boolean matchesPathPattern(String pattern, String requestPath) {
97+
// Normalize both paths to prevent path traversal attacks
98+
String normalizedPattern = normalizePath(pattern);
99+
String normalizedRequestPath = normalizePath(requestPath);
100+
101+
// Split both paths into segments
102+
String[] patternSegments = normalizedPattern.split("/", -1); // keep trailing empty segments
103+
String[] requestSegments = normalizedRequestPath.split("/", -1);
104+
105+
// Paths must have the same number of segments
106+
if (patternSegments.length != requestSegments.length) {
107+
return false;
108+
}
109+
110+
// Compare each segment
111+
for (int i = 0; i < patternSegments.length; i++) {
112+
String patternSegment = patternSegments[i];
113+
String requestSegment = requestSegments[i];
114+
115+
// If both are empty (leading/trailing slash), continue
116+
if (patternSegment.isEmpty() && requestSegment.isEmpty()) {
117+
continue;
118+
}
119+
// If pattern segment is a path parameter (enclosed in {}), it matches any non-empty segment
120+
if (isPathParameter(patternSegment)) {
121+
if (requestSegment.isEmpty()) {
122+
return false;
123+
}
124+
} else {
125+
// For literal segments, they must match exactly
126+
if (!patternSegment.equals(requestSegment)) {
127+
return false;
128+
}
129+
}
130+
}
131+
132+
return true;
133+
}
134+
135+
/**
136+
* Normalizes a path to prevent path traversal attacks. This method: 1. URL decodes
137+
* percent-encoded characters 2. Normalizes multiple consecutive slashes to single slashes 3.
138+
* Resolves path traversal sequences (../) 4. Ensures the path doesn't escape the root directory
139+
*
140+
* @param path The path to normalize
141+
* @return The normalized path
142+
* @throws IllegalArgumentException if the path contains invalid traversal sequences
143+
*/
144+
private String normalizePath(String path) {
145+
if (path == null || path.isEmpty()) {
146+
return "/";
147+
}
148+
149+
try {
150+
// URL decode the path to handle percent-encoded characters like %2F
151+
String decodedPath = URLDecoder.decode(path, StandardCharsets.UTF_8);
152+
153+
// Normalize multiple consecutive slashes to single slashes
154+
String normalizedPath = decodedPath.replaceAll("/+", "/");
155+
156+
// Split into segments and resolve path traversal
157+
String[] segments = normalizedPath.split("/");
158+
List<String> resolvedSegments = new ArrayList<>();
159+
160+
for (String segment : segments) {
161+
if (segment.isEmpty() || ".".equals(segment)) {
162+
// Skip empty segments and current directory references
163+
continue;
164+
} else if ("..".equals(segment)) {
165+
// Go up one directory level
166+
if (!resolvedSegments.isEmpty()) {
167+
resolvedSegments.remove(resolvedSegments.size() - 1);
168+
} else {
169+
// Attempting to go above root - this is a security violation
170+
throw new IllegalArgumentException("Path traversal attack detected: " + path);
171+
}
172+
} else {
173+
// Add normal segment
174+
resolvedSegments.add(segment);
175+
}
176+
}
177+
178+
// Reconstruct the path
179+
StringBuilder result = new StringBuilder();
180+
for (String segment : resolvedSegments) {
181+
result.append("/").append(segment);
182+
}
183+
184+
// Ensure the result starts with / and handle empty path case
185+
String finalPath = result.toString();
186+
return finalPath.isEmpty() ? "/" : finalPath;
187+
188+
} catch (Exception e) {
189+
// If URL decoding fails or any other error occurs, throw security exception
190+
throw new IllegalArgumentException("Invalid path format: " + path, e);
191+
}
192+
}
193+
194+
/**
195+
* Checks if a path segment is a path parameter (enclosed in curly braces).
196+
*
197+
* @param segment The path segment to check
198+
* @return true if it's a path parameter
199+
*/
200+
private boolean isPathParameter(String segment) {
201+
return segment.startsWith("{") && segment.endsWith("}") && segment.length() > 2;
202+
}
203+
204+
@Override
205+
public String toString() {
206+
return method + ":" + path;
207+
}
208+
209+
@Override
210+
public boolean equals(Object obj) {
211+
if (this == obj) return true;
212+
if (obj == null || getClass() != obj.getClass()) return false;
213+
BlockedRoute that = (BlockedRoute) obj;
214+
return method.equals(that.method) && path.equals(that.path);
215+
}
216+
217+
@Override
218+
public int hashCode() {
219+
return method.hashCode() * 31 + path.hashCode();
220+
}
221+
}

0 commit comments

Comments
 (0)