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]

Reply via email to