This is an automated email from the ASF dual-hosted git repository.
pjfanning pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko-http.git
The following commit(s) were added to refs/heads/main by this push:
new 28def88cf fix: require every Origin to be allowed on a CORS request
(#1262)
28def88cf is described below
commit 28def88cf11835602c14b0fb62039455b2496915
Author: PJ Fanning <[email protected]>
AuthorDate: Sun Sep 6 12:02:55 2026 +0100
fix: require every Origin to be allowed on a CORS request (#1262)
Motivation:
`validateOrigins` accepted a request as soon as *one* of the origins in
the `Origin` header matched the allowed matcher, but the response
echoes back *every* origin it was given
(`CorsSettingsImpl.accessControlAllowOrigin` renders the whole list). A
request carrying an allowed origin next to a disallowed one therefore
passed validation and echoed the disallowed origin back in
`Access-Control-Allow-Origin`. The pre-flight path is not affected
because it already only accepts a single origin, so this was limited to
simple/actual requests.
Modification:
Require all origins to match rather than any, keeping the existing
`allowed-origins = *` short-circuit. The extra `nonEmpty` guard
preserves today's handling of a `null` origin (an empty origin list),
which must still be rejected when the allowed origins are restricted -
`forall` alone would accept it.
Result:
An actual request is accepted only when every origin it lists is
allowed, so no origin that failed validation is echoed back. A request
whose origins are all disallowed is still rejected with
`InvalidOrigin` listing them, as before.
Tests:
- sbt http-cors/test - pass (47 tests); a new test asserts that an actual
request listing a disallowed origin next to an allowed one is rejected with
`InvalidOrigin`. Verified it fails with the fix stashed (the request is
accepted and both origins are echoed). The existing "there are two origins"
rejection test and the restricted-`null`-origin test both still pass.
- sbt http-cors/mimaReportBinaryIssues - pass
References:
None - requires all origins of a CORS request to be allowed
---
.../apache/pekko/http/cors/scaladsl/CorsDirectives.scala | 6 ++++--
.../org/apache/pekko/http/cors/CorsDirectivesSpec.scala | 13 +++++++++++++
2 files changed, 17 insertions(+), 2 deletions(-)
diff --git
a/http-cors/src/main/scala/org/apache/pekko/http/cors/scaladsl/CorsDirectives.scala
b/http-cors/src/main/scala/org/apache/pekko/http/cors/scaladsl/CorsDirectives.scala
index c1c6c6f96..1c830189c 100644
---
a/http-cors/src/main/scala/org/apache/pekko/http/cors/scaladsl/CorsDirectives.scala
+++
b/http-cors/src/main/scala/org/apache/pekko/http/cors/scaladsl/CorsDirectives.scala
@@ -67,9 +67,11 @@ trait CorsDirectives {
def cors(settings: CorsSettings): Directive0 = {
import settings._
- // Return the invalid origins, or `Nil` if one is valid.
+ // Return the invalid origins, or `Nil` if they are all valid. Every
origin has to match, not just one of them:
+ // the response echoes back every origin given, so accepting on a single
match would echo an origin that was
+ // never allowed when a request lists a permitted origin alongside a
disallowed one.
def validateOrigins(origins: Seq[HttpOrigin]): List[CorsRejection.Cause] =
- if (allowedOrigins == HttpOriginMatcher.* ||
origins.exists(allowedOrigins.matches)) {
+ if (allowedOrigins == HttpOriginMatcher.* || (origins.nonEmpty &&
origins.forall(allowedOrigins.matches))) {
Nil
} else {
CorsRejection.InvalidOrigin(origins) :: Nil
diff --git
a/http-cors/src/test/scala/org/apache/pekko/http/cors/CorsDirectivesSpec.scala
b/http-cors/src/test/scala/org/apache/pekko/http/cors/CorsDirectivesSpec.scala
index 41a366724..b75ddd83f 100644
---
a/http-cors/src/test/scala/org/apache/pekko/http/cors/CorsDirectivesSpec.scala
+++
b/http-cors/src/test/scala/org/apache/pekko/http/cors/CorsDirectivesSpec.scala
@@ -106,6 +106,19 @@ class CorsDirectivesSpec extends AnyWordSpec with Matchers
with Directives with
}
}
+ "reject an actual request listing a disallowed origin next to an allowed
one" in {
+ val settings =
referenceSettings.withAllowedOrigins(HttpOriginMatcher(exampleOrigin))
+ val disallowedOrigin = HttpOrigin("http://evil.com")
+
+ // the response echoes back every origin given, so accepting because one
of them matches would echo the
+ // disallowed origin back to the caller
+ Get() ~> Origin(Seq(exampleOrigin, disallowedOrigin)) ~> {
+ route(settings)
+ } ~> check {
+ rejection shouldBe
CorsRejection(CorsRejection.InvalidOrigin(Seq(exampleOrigin, disallowedOrigin)))
+ }
+ }
+
"accept pre-flight requests with a null origin when allowed-origins = `*`"
in {
val settings = referenceSettings
Options() ~> Origin(Seq.empty) ~> `Access-Control-Request-Method`(GET)
~> {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]