Skip to content

Commit 7fe674e

Browse files
committed
Fix KTOR-9659 CORS is skipped when the Origin header contains an IPv6 address
1 parent 9672983 commit 7fe674e

2 files changed

Lines changed: 83 additions & 14 deletions

File tree

  • ktor-server
    • ktor-server-plugins/ktor-server-cors/common/src/io/ktor/server/plugins/cors
    • ktor-server-tests/common/test/io/ktor/tests/server/plugins

ktor-server/ktor-server-plugins/ktor-server-cors/common/src/io/ktor/server/plugins/cors/CORSUtils.kt

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,10 @@ internal fun corsCheckOrigins(
7373
}
7474
} else {
7575
when {
76-
allowsAnyHost ->
77-
LOGGER.trace { "${request.id()}: Any * host is allowed" }
76+
allowsAnyHost -> LOGGER.trace { "${request.id()}: Any * host is allowed" }
7877
normalizedOrigin in hostsNormalized ->
7978
LOGGER.trace { "${request.id()}: Origin $normalizedOrigin is allowed from $hostsNormalized" }
79+
8080
matchWildcardHosts ->
8181
LOGGER.trace {
8282
val (prefix, suffix) = hostsWithWildcard
@@ -85,6 +85,7 @@ internal fun corsCheckOrigins(
8585
}!!
8686
"${request.id()}: Origin $normalizedOrigin matches wildcard host $prefix*$suffix"
8787
}
88+
8889
originPredicates.any { it(origin) } -> {
8990
LOGGER.trace {
9091
"${request.id()}: Origin $normalizedOrigin fulfills " +
@@ -134,6 +135,27 @@ internal suspend fun ApplicationCall.respondCorsFailed() {
134135
respond(HttpStatusCode.Forbidden)
135136
}
136137

138+
private fun findPortDigitStartIndex(origin: String, hostStartIndex: Int): Int {
139+
val isIpv6 = hostStartIndex < origin.length && origin[hostStartIndex] == '['
140+
if (isIpv6) {
141+
val ipv6LiteralEndIndex = origin.indexOf(']', hostStartIndex)
142+
if (ipv6LiteralEndIndex == -1) {
143+
return -1
144+
}
145+
val portSeparatorIndex = origin.indexOf(':', ipv6LiteralEndIndex)
146+
return if (portSeparatorIndex != -1) portSeparatorIndex + 1 else origin.length
147+
}
148+
149+
for (index in hostStartIndex until origin.length) {
150+
when (origin[index]) {
151+
':' -> return index + 1
152+
'/' -> return origin.length
153+
'?' -> return -1
154+
}
155+
}
156+
return origin.length
157+
}
158+
137159
internal fun isValidOrigin(origin: String): Boolean {
138160
if (origin.isEmpty()) return false
139161
if (origin == "null") return true
@@ -148,17 +170,11 @@ internal fun isValidOrigin(origin: String): Boolean {
148170

149171
if (!protoValid) return false
150172

151-
var portIndex = origin.length
152-
for (index in protoDelimiter + 3 until origin.length) {
153-
val ch = origin[index]
154-
if (ch == ':' || ch == '/') {
155-
portIndex = index + 1
156-
break
157-
}
158-
if (ch == '?') return false
159-
}
173+
val hostStartIndex = protoDelimiter + 3
174+
val portDigitStartIndex = findPortDigitStartIndex(origin, hostStartIndex)
175+
if (portDigitStartIndex == -1) return false
160176

161-
for (index in portIndex until origin.length) {
177+
for (index in portDigitStartIndex until origin.length) {
162178
val isTrailingSlash = index == origin.length - 1 && origin[index] == '/'
163179
if (!origin[index].isDigit() && !isTrailingSlash) return false
164180
}
@@ -175,8 +191,13 @@ internal fun normalizeOrigin(origin: String): String {
175191
} else {
176192
builder.append(origin)
177193
}
178-
if (!builder.toString().substringAfterLast(":", "").matches(NUMBER_REGEX)) {
179-
val port = when (builder.toString().substringBefore(':')) {
194+
val originWithoutTrailingSlash = builder.toString()
195+
val hostStartIndex = originWithoutTrailingSlash.indexOf("://") + 3
196+
val portDigitStartIndex = findPortDigitStartIndex(originWithoutTrailingSlash, hostStartIndex)
197+
val hasExplicitPort = portDigitStartIndex in originWithoutTrailingSlash.indices &&
198+
originWithoutTrailingSlash.substring(portDigitStartIndex).matches(NUMBER_REGEX)
199+
if (!hasExplicitPort) {
200+
val port = when (originWithoutTrailingSlash.substringBefore(':')) {
180201
"http" -> "80"
181202
"https" -> "443"
182203
else -> null

ktor-server/ktor-server-tests/common/test/io/ktor/tests/server/plugins/CORSTest.kt

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -996,6 +996,54 @@ class CORSTest {
996996
}
997997
}
998998

999+
@Test
1000+
fun ipv6LiteralOriginIsAccepted() = testApplication {
1001+
install(CORS) {
1002+
anyHost()
1003+
}
1004+
1005+
routing {
1006+
get("/") {
1007+
call.respond("OK")
1008+
}
1009+
}
1010+
1011+
client.get("/") {
1012+
header(HttpHeaders.Origin, "http://[::1]:22222")
1013+
}.let { call ->
1014+
assertEquals(HttpStatusCode.OK, call.status)
1015+
assertEquals("*", call.headers[HttpHeaders.AccessControlAllowOrigin])
1016+
}
1017+
1018+
client.get("/") {
1019+
header(HttpHeaders.Origin, "http://[2001:db8::1]:8080")
1020+
}.let { call ->
1021+
assertEquals(HttpStatusCode.OK, call.status)
1022+
assertEquals("*", call.headers[HttpHeaders.AccessControlAllowOrigin])
1023+
}
1024+
1025+
client.get("/") {
1026+
header(HttpHeaders.Origin, "http://[::1]:22222/")
1027+
}.let { call ->
1028+
assertEquals(HttpStatusCode.OK, call.status)
1029+
assertEquals("*", call.headers[HttpHeaders.AccessControlAllowOrigin])
1030+
}
1031+
1032+
client.get("/") {
1033+
header(HttpHeaders.Origin, "http://[::1]:notaport")
1034+
}.let { call ->
1035+
assertEquals(HttpStatusCode.OK, call.status)
1036+
assertNull(call.headers[HttpHeaders.AccessControlAllowOrigin])
1037+
}
1038+
1039+
client.get("/") {
1040+
header(HttpHeaders.Origin, "http://[::1")
1041+
}.let { call ->
1042+
assertEquals(HttpStatusCode.OK, call.status)
1043+
assertNull(call.headers[HttpHeaders.AccessControlAllowOrigin])
1044+
}
1045+
}
1046+
9991047
@Test
10001048
fun originValidation() = testApplication {
10011049
install(CORS) {

0 commit comments

Comments
 (0)