Skip to content

Commit 98f2433

Browse files
Mateusz Krawieccopybara-github
authored andcommitted
fix(dev): default adk.web.cors.origins to same-origin only instead of *
- Applies to the REST API and the `/run_live` WebSocket. The bundled dev UI is same-origin and needs no configuration. - A UI served from another origin now needs `adk.web.cors.origins`, for example `adk.web.cors.origins=http://localhost:4200`. - Behind a reverse proxy, a request whose origin matches the URL in the proxy's `Forwarded` header, or its `X-Forwarded-*` headers when `Forwarded` is absent, counts as same-origin. A proxy that hides the external scheme, host or non-default port needs `adk.web.cors.origins`. PiperOrigin-RevId: 997788515
1 parent 97f22a2 commit 98f2433

7 files changed

Lines changed: 530 additions & 8 deletions

File tree

‎dev/README.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,24 @@ and the bundled `backendUrl` value is preserved. If you instead enable
3939
`server.forward-headers-strategy=framework`, make sure your reverse proxy strips
4040
or overwrites incoming `Forwarded` and `X-Forwarded-*` headers from untrusted
4141
clients.
42+
43+
## Cross-origin requests
44+
45+
By default, the REST API and the `/run_live` WebSocket accept only same-origin
46+
requests. The dev UI served by the server itself is same-origin and needs no
47+
configuration.
48+
49+
To allow other origins, for example a UI served by a separate development
50+
server, set `adk.web.cors.origins` to a comma-separated list of origins:
51+
52+
```properties
53+
adk.web.cors.origins=http://localhost:4200
54+
```
55+
56+
Behind a reverse proxy, a request also counts as same-origin when its origin
57+
matches the URL that the proxy reports in the `Forwarded` header, or in
58+
`X-Forwarded-*` headers when `Forwarded` is absent. The dev UI therefore keeps
59+
working behind a proxy that terminates TLS. List the external origin in
60+
`adk.web.cors.origins` if the proxy changes the scheme, host or port without
61+
reporting it; a reported scheme or host without a port implies the scheme's
62+
default port. Avoid `*`: it lets any website open in your browser call the server.

‎dev/src/main/java/com/google/adk/web/config/AdkWebCorsConfig.java‎

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,16 @@
1616

1717
package com.google.adk.web.config;
1818

19+
import java.io.IOException;
1920
import org.slf4j.Logger;
2021
import org.slf4j.LoggerFactory;
2122
import org.springframework.context.annotation.Bean;
2223
import org.springframework.context.annotation.Configuration;
24+
import org.springframework.http.server.ServerHttpRequest;
25+
import org.springframework.http.server.ServerHttpResponse;
2326
import org.springframework.web.cors.CorsConfiguration;
2427
import org.springframework.web.cors.CorsConfigurationSource;
28+
import org.springframework.web.cors.DefaultCorsProcessor;
2529
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
2630
import org.springframework.web.filter.CorsFilter;
2731

@@ -50,10 +54,10 @@ public class AdkWebCorsConfig {
5054
public CorsConfigurationSource corsConfigurationSource(AdkWebCorsProperties corsProperties) {
5155
if (corsProperties.origins().contains("*")) {
5256
logger.warn(
53-
"CORS is configured to allow all origins (\"*\"), which is insecure and intended for"
54-
+ " local development only. This also applies to the /run_live WebSocket endpoint."
55-
+ " Set 'adk.web.cors.origins' to an explicit allowlist to restrict which origins"
56-
+ " may call the server.");
57+
"CORS is configured to allow all origins (\"*\"), so any website open in a browser can"
58+
+ " call this server, including the /run_live WebSocket endpoint. Set"
59+
+ " 'adk.web.cors.origins' to an explicit allowlist to restrict which origins may"
60+
+ " call the server.");
5761
}
5862

5963
CorsConfiguration configuration = new CorsConfiguration();
@@ -72,6 +76,23 @@ public CorsConfigurationSource corsConfigurationSource(AdkWebCorsProperties cors
7276

7377
@Bean
7478
public CorsFilter corsFilter(CorsConfigurationSource corsConfigurationSource) {
75-
return new CorsFilter(corsConfigurationSource);
79+
CorsFilter filter = new CorsFilter(corsConfigurationSource);
80+
filter.setCorsProcessor(new ForwardedSameOriginCorsProcessor());
81+
return filter;
82+
}
83+
84+
/** Treats a request as same-origin when its origin matches the URL a reverse proxy reports. */
85+
private static final class ForwardedSameOriginCorsProcessor extends DefaultCorsProcessor {
86+
87+
@Override
88+
protected boolean handleInternal(
89+
ServerHttpRequest request,
90+
ServerHttpResponse response,
91+
CorsConfiguration config,
92+
boolean preFlightRequest)
93+
throws IOException {
94+
return ForwardedSameOrigin.matches(request)
95+
|| super.handleInternal(request, response, config, preFlightRequest);
96+
}
7697
}
7798
}

‎dev/src/main/java/com/google/adk/web/config/AdkWebCorsProperties.java‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121

2222
/**
2323
* Properties for configuring CORS in ADK Web. This class is used to load CORS settings from
24-
* application properties.
24+
* application properties. {@code origins} defaults to an empty list, which allows only same-origin
25+
* requests.
2526
*/
2627
@ConfigurationProperties(prefix = "adk.web.cors")
2728
public record AdkWebCorsProperties(
@@ -34,7 +35,7 @@ public record AdkWebCorsProperties(
3435

3536
public AdkWebCorsProperties {
3637
mapping = mapping != null ? mapping : "/**";
37-
origins = origins != null && !origins.isEmpty() ? origins : List.of("*");
38+
origins = origins != null ? origins : List.of();
3839
methods =
3940
methods != null && !methods.isEmpty()
4041
? methods
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.adk.web.config;
18+
19+
import java.net.URI;
20+
import org.springframework.http.HttpRequest;
21+
import org.springframework.http.client.support.HttpRequestWrapper;
22+
import org.springframework.web.util.ForwardedHeaderUtils;
23+
import org.springframework.web.util.UriComponentsBuilder;
24+
import org.springframework.web.util.WebUtils;
25+
26+
/**
27+
* Same-origin check against the URL that a reverse proxy reports in the {@code Forwarded} header,
28+
* or in {@code X-Forwarded-*} headers when {@code Forwarded} is absent. Browsers let a cross-origin
29+
* page send those headers only after a CORS preflight, which fails the origin check, so such a page
30+
* cannot fake a match. Other clients can send any value, so this is not an authorization check.
31+
*/
32+
public final class ForwardedSameOrigin {
33+
34+
/**
35+
* Returns whether the {@code Origin} header matches the request URL as the proxy reports it. A
36+
* request without an {@code Origin} header matches; one with a header Spring cannot parse does
37+
* not.
38+
*/
39+
public static boolean matches(HttpRequest request) {
40+
if (request.getHeaders().getOrigin() == null) {
41+
return true;
42+
}
43+
try {
44+
UriComponentsBuilder forwarded =
45+
ForwardedHeaderUtils.adaptFromForwardedHeaders(request.getURI(), request.getHeaders());
46+
// Some proxies report a WebSocket handshake as ws or wss; the browser's Origin uses http(s).
47+
String scheme = forwarded.build().getScheme();
48+
if ("ws".equalsIgnoreCase(scheme)) {
49+
forwarded.scheme("http");
50+
} else if ("wss".equalsIgnoreCase(scheme)) {
51+
forwarded.scheme("https");
52+
}
53+
URI forwardedUri = forwarded.build().toUri();
54+
return WebUtils.isSameOrigin(
55+
new HttpRequestWrapper(request) {
56+
@Override
57+
public URI getURI() {
58+
return forwardedUri;
59+
}
60+
});
61+
} catch (IllegalArgumentException | IllegalStateException | IndexOutOfBoundsException e) {
62+
// Spring's parsers throw these for a malformed Origin, forwarded header or request URL.
63+
return false;
64+
}
65+
}
66+
67+
private ForwardedSameOrigin() {}
68+
}

‎dev/src/main/java/com/google/adk/web/websocket/WebSocketConfig.java‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,18 @@
1717
package com.google.adk.web.websocket;
1818

1919
import com.google.adk.web.config.AdkWebCorsProperties;
20+
import com.google.adk.web.config.ForwardedSameOrigin;
21+
import java.util.List;
22+
import java.util.Map;
2023
import org.springframework.beans.factory.annotation.Autowired;
2124
import org.springframework.context.annotation.Configuration;
25+
import org.springframework.http.server.ServerHttpRequest;
26+
import org.springframework.http.server.ServerHttpResponse;
27+
import org.springframework.web.socket.WebSocketHandler;
2228
import org.springframework.web.socket.config.annotation.EnableWebSocket;
2329
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
2430
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
31+
import org.springframework.web.socket.server.support.OriginHandshakeInterceptor;
2532

2633
/** Configuration class for WebSocket handling. */
2734
@Configuration
@@ -42,6 +49,27 @@ public WebSocketConfig(
4249
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
4350
registry
4451
.addHandler(liveWebSocketHandler, "/run_live")
45-
.setAllowedOrigins(corsProperties.origins().toArray(new String[0]));
52+
.addInterceptors(new ForwardedAwareOriginInterceptor(corsProperties.origins()))
53+
// The interceptor checks origins; the registry's own check ignores forwarded headers.
54+
.setAllowedOrigins("*");
55+
}
56+
57+
/** Origin check for the handshake that also accepts the URL a reverse proxy reports. */
58+
private static final class ForwardedAwareOriginInterceptor extends OriginHandshakeInterceptor {
59+
60+
ForwardedAwareOriginInterceptor(List<String> allowedOrigins) {
61+
super(allowedOrigins);
62+
}
63+
64+
@Override
65+
public boolean beforeHandshake(
66+
ServerHttpRequest request,
67+
ServerHttpResponse response,
68+
WebSocketHandler wsHandler,
69+
Map<String, Object> attributes)
70+
throws Exception {
71+
return ForwardedSameOrigin.matches(request)
72+
|| super.beforeHandshake(request, response, wsHandler, attributes);
73+
}
4674
}
4775
}

0 commit comments

Comments
 (0)