Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package org.pkl.core.http;

import com.google.errorprone.annotations.ThreadSafe;
import java.io.Closeable;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
Expand Down Expand Up @@ -134,12 +135,16 @@ private <T> HttpResponse<T> doSend(
throw new HttpClientException(
ErrorMessages.create("httpRedirectInvalidUri", currentRequestUri, location.get()));
}
var nextRequestUri = rewriteUri(redirectUri);
if (currentRequestUri.getScheme().equalsIgnoreCase("https")
&& redirectUri.getScheme().equalsIgnoreCase("http")) {
&& nextRequestUri.getScheme().equalsIgnoreCase("http")) {
Comment on lines +138 to +140

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current behavior is actually is intentional; if a user rewrites a request URI, they're on the hook for the security implications there.

You can already downgrade when not in the context of redirect following; just because we're following redirects here shouldn't change that.

throw new HttpClientException(
ErrorMessages.create("httpRedirectCannotDowngrade", currentRequestUri, redirectUri));
ErrorMessages.create("httpRedirectCannotDowngrade", currentRequestUri, nextRequestUri));
}
currentRequestUri = rewriteUri(redirectUri);
if (response.body() instanceof Closeable closeable) {
closeable.close();
}
Comment on lines +144 to +146

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this to line 122; otherwise the throws here still won't close the response body.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably prudent to stick this in a finally block instead

currentRequestUri = nextRequestUri;
currentRequest = rewriteRequest(request, currentRequestUri);
redirectCount++;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package org.pkl.core.http

import java.io.ByteArrayInputStream
import java.io.InputStream
import java.net.URI
import java.net.http.HttpClient as JdkHttpClient
import java.net.http.HttpRequest
Expand All @@ -23,8 +25,10 @@ import java.net.http.HttpResponse.BodyHandlers
import java.time.Duration
import java.util.Locale
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatCode
import org.assertj.core.api.Assertions.assertThatList
import org.junit.jupiter.api.Test
import org.pkl.commons.test.FakeHttpResponse
import org.pkl.core.util.GlobResolver
import org.pkl.core.util.IoUtils

Expand Down Expand Up @@ -441,4 +445,65 @@ class RequestRewritingClientTest {
assertThatList(captured.request.headers().allValues("user-agent"))
.containsExactly("My User Agent")
}

@Test
fun `rejects downgrade when a rewrite maps an https redirect target to http`() {
val delegate =
ReplayingClient(
FakeHttpResponse<Void>().apply {
statusCode = 302
headers = httpHeaders("Location", "https://redirect-target/bar.pkl")
},
FakeHttpResponse<Void>(),
)
val client =
RequestRewritingClient(
"Pkl",
Duration.ofSeconds(42),
-1,
delegate,
mapOf(URI("https://redirect-target/") to URI("http://downgraded/")),
mapOf(),
)
val request = HttpRequest.newBuilder(URI("https://start.example/foo.pkl")).build()

assertThatCode { client.send(request, BodyHandlers.discarding(), NoopChecker) }
.`as`("following an https redirect whose rewritten target is http must be rejected")
.hasMessageContaining("Cannot follow redirect from 'https:' URL to 'http:' URL")
assertThat(delegate.requestedUris.map { it.scheme }).doesNotContain("http")
}

@Test
fun `closes intermediate redirect response body before following the redirect`() {
val intermediateBody = CloseTrackingInputStream()
val delegate =
ReplayingClient(
FakeHttpResponse<InputStream>().apply {
statusCode = 302
headers = httpHeaders("Location", "/bar.pkl")
body = intermediateBody
},
FakeHttpResponse<InputStream>(),
)
val client =
RequestRewritingClient("Pkl", Duration.ofSeconds(42), -1, delegate, mapOf(), mapOf())
val request = HttpRequest.newBuilder(URI("https://example.com/foo.pkl")).build()

client.send(request, BodyHandlers.ofInputStream(), NoopChecker)

assertThat(intermediateBody.closed)
.`as`("intermediate 3xx response body must be closed to release its connection")
.isTrue
}

/** @ByteArrayInputStream that records whether it was closed. */
private class CloseTrackingInputStream : ByteArrayInputStream(ByteArray(0)) {
var closed = false
private set

override fun close() {
closed = true
super.close()
}
}
}
23 changes: 23 additions & 0 deletions pkl-core/src/test/kotlin/org/pkl/core/http/util.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
package org.pkl.core.http

import java.net.URI
import java.net.http.HttpHeaders
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.util.regex.Pattern

data class HttpSettings(
Expand All @@ -32,3 +35,23 @@ fun HttpClient.getConfiguredSettings(): HttpSettings {
object NoopChecker : HttpClient.HttpRequestChecker {
override fun check(uri: URI) {}
}

fun httpHeaders(name: String, value: String): HttpHeaders =
HttpHeaders.of(mapOf(name to listOf(value))) { _, _ -> true }

class ReplayingClient(vararg responses: HttpResponse<*>) : HttpClient {
val requestedUris = mutableListOf<URI>()
private val responses = ArrayDeque(responses.toList())

@Suppress("UNCHECKED_CAST")
override fun <T : Any> send(
request: HttpRequest,
responseBodyHandler: HttpResponse.BodyHandler<T>,
httpRequestChecker: HttpClient.HttpRequestChecker,
): HttpResponse<T> {
requestedUris.add(request.uri())
return responses.removeFirst() as HttpResponse<T>
}

override fun close() {}
}
Loading