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
4 changes: 2 additions & 2 deletions server/lib/cdpmonitor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -359,8 +359,8 @@ Unless otherwise noted, events also include the nav context fields described abo

| Event | Fields |
| --- | --- |
| `network_request` | `request_id`, `loader_id`, `frame_id`, `document_url`, `method`, `url`, `headers`, `initiator_type`. Optional: `post_data`, `resource_type`, `is_redirect` + `redirect_url`. |
| `network_response` | `request_id`, `loader_id`, `frame_id`, `method`, `url`, `status`, `headers`. Optional: `status_text`, `mime_type`, `resource_type`, `body` (truncated text body for textual MIME types). |
| `network_request` | `request_id`, `loader_id`, `frame_id`, `document_url`, `method`, `url`, `headers`, `initiator_type`. Optional: `resource_type`, `is_redirect` + `redirect_url`. The request body is not published, and `headers` leaves out `Authorization`, `Proxy-Authorization` and `Cookie`. |
| `network_response` | `request_id`, `loader_id`, `frame_id`, `method`, `url`, `status`, `headers` (without `Set-Cookie`). Optional: `status_text`, `mime_type`, `resource_type`, `body` (truncated text body for textual MIME types). |
| `network_loading_failed` | `request_id`, `error_text`, `canceled`. Optional (absent when the request record was not found): `url`, `loader_id`, `frame_id`, `resource_type`. |
| `proxy_error` | `request_id`, `code` (typed enum matching the metro header values), `status` (502, or 403 when the session's network policy blocks the destination). Optional: `raw_code` (sanitized original value when `code` is `unknown`), `url`, `loader_id`, `frame_id`, `method`, `resource_type`. Emitted when a 502 or 403 response carries the `X-Kernel-Proxy-Error` header. Unrecognized values are reported as `unknown`; all such values share one rate-limit slot. Emission is sampled to at most one per session+code+resource_type per second. WebSocket handshakes are not classified (documented non-goal). A CONNECT the proxy refuses outright, such as a `network_policy_denied` CONNECT to a port other than 443, is not classified either: Chromium does not expose the refusal, so it appears only as `network_loading_failed` with `net::ERR_TUNNEL_CONNECTION_FAILED`. |

Expand Down
20 changes: 5 additions & 15 deletions server/lib/cdpmonitor/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -419,11 +419,9 @@ func (m *Monitor) handleTimelineEvent(p cdpPerformanceTimelineEventAddedParams,
}
}

// handleNetworkRequest publishes network_request events.
// NOTE: events include raw headers, post_data, and (on response) truncated
// bodies which may contain cookies, bearer tokens, or other credentials.
// This mirrors what CDP/DevTools itself exposes. Consumers should treat the
// event stream as privileged data; opt-in redaction can be added later.
// handleNetworkRequest publishes network_request events. Credential headers are
// left out: the telemetry reader may not own them. Request bodies are left out
// until a scrubber can remove the credentials in them.
func (m *Monitor) handleNetworkRequest(p cdpNetworkRequestWillBeSentParams, sessionID string) {
// Extract only the initiator type; the stack trace is too verbose and dominates event size.
var initiatorType string
Expand Down Expand Up @@ -461,16 +459,13 @@ func (m *Monitor) handleNetworkRequest(p cdpNetworkRequestWillBeSentParams, sess
method: p.Request.Method,
url: p.Request.URL,
headers: p.Request.Headers,
postData: p.Request.PostData,
resourceType: p.Type,
loaderID: p.LoaderID,
frameID: p.FrameID,
navSeq: navSeq,
addedAt: addedAt,
}
m.pendReqMu.Unlock()
var hdrs oapi.BrowserHttpHeaders
_ = json.Unmarshal(p.Request.Headers, &hdrs)
payload := oapi.BrowserNetworkRequestEventData{
SessionId: sessionID,
TargetId: info.targetID,
Expand All @@ -482,12 +477,9 @@ func (m *Monitor) handleNetworkRequest(p cdpNetworkRequestWillBeSentParams, sess
RequestId: p.RequestID,
Method: p.Request.Method,
DocumentUrl: p.DocumentURL,
Headers: hdrs,
Headers: publishedHeaders(p.Request.Headers),
InitiatorType: initiatorType,
}
if p.Request.PostData != "" {
payload.PostData = ptrOf(p.Request.PostData)
}
if p.Type != "" {
payload.ResourceType = ptrOf(p.Type)
}
Expand Down Expand Up @@ -577,8 +569,6 @@ func (m *Monitor) handleLoadingFinished(ctx context.Context, p cdpNetworkLoading
// Fetch response body async to avoid blocking readLoop; binary types are skipped.
m.telemetryWg.Go(func() {
body := m.fetchResponseBody(ctx, p.RequestID, sessionID, state)
var hdrs oapi.BrowserHttpHeaders
_ = json.Unmarshal(state.resHeaders, &hdrs)
resPayload := oapi.BrowserNetworkResponseEventData{
SessionId: sessionID,
TargetId: info.targetID,
Expand All @@ -590,7 +580,7 @@ func (m *Monitor) handleLoadingFinished(ctx context.Context, p cdpNetworkLoading
RequestId: p.RequestID,
Method: state.method,
Status: state.status,
Headers: hdrs,
Headers: publishedHeaders(state.resHeaders),
}
if state.statusText != "" {
resPayload.StatusText = ptrOf(state.statusText)
Expand Down
47 changes: 47 additions & 0 deletions server/lib/cdpmonitor/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,53 @@ func TestNetworkEvents(t *testing.T) {
assert.Equal(t, "net::ERR_CONNECTION_REFUSED", data["error_text"])
})

t.Run("credentials_left_out", func(t *testing.T) {
cp := ec.checkpoint()
srv.sendToMonitor(t, map[string]any{
"method": "Network.requestWillBeSent",
"params": map[string]any{
"requestId": "req-creds",
"type": "XHR",
"request": map[string]any{
"method": "POST",
"url": "https://login.example.com/session",
"postData": `{"password":"canary-password"}`,
"headers": map[string]any{
"Content-Type": "application/json",
"authorization": "Bearer canary-token",
"Proxy-Authorization": "Basic canary-proxy",
"COOKIE": "sid=canary-cookie",
},
},
},
})
ev := ec.waitForNew(t, "network_request", cp, 2*time.Second)
var data map[string]any
require.NoError(t, json.Unmarshal(ev.Data, &data))
assert.NotContains(t, data, "post_data")
assert.Equal(t, map[string]any{"Content-Type": "application/json"}, data["headers"])

srv.sendToMonitor(t, map[string]any{
"method": "Network.responseReceived",
"params": map[string]any{
"requestId": "req-creds",
"response": map[string]any{
"status": 200, "statusText": "OK", "mimeType": "application/json",
"headers": map[string]any{"Content-Type": "application/json", "Set-Cookie": "sid=canary-cookie"},
},
},
})
srv.sendToMonitor(t, map[string]any{
"method": "Network.loadingFinished",
"params": map[string]any{"requestId": "req-creds"},
})
ev2 := ec.waitForNew(t, "network_response", cp, 3*time.Second)
assert.NotContains(t, string(ev2.Data), "canary")
var data2 map[string]any
require.NoError(t, json.Unmarshal(ev2.Data, &data2))
assert.Equal(t, map[string]any{"Content-Type": "application/json"}, data2["headers"])
})

t.Run("binary_resource_skips_body", func(t *testing.T) {
getBodyCalled.Store(false)
cp := ec.checkpoint()
Expand Down
3 changes: 1 addition & 2 deletions server/lib/cdpmonitor/network_chrome_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,7 @@ func TestNetworkCaptureAcrossFrames(t *testing.T) {
var request oapi.BrowserNetworkRequestEventData
require.NoError(t, json.Unmarshal(requestEvent.Data, &request))
require.Equal(t, http.MethodPost, request.Method)
require.NotNil(t, request.PostData)
require.JSONEq(t, string(body), *request.PostData)
require.Nil(t, request.PostData, "request bodies are not published")
require.Equal(t, frame.target, request.TargetId)
require.Equal(t, frame.targetType, string(request.TargetType))
require.True(t, request.TargetType.Valid())
Expand Down
1 change: 0 additions & 1 deletion server/lib/cdpmonitor/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@ type networkReqState struct {
method string
url string
headers json.RawMessage
postData string
resourceType string
loaderID string
frameID string
Expand Down
16 changes: 16 additions & 0 deletions server/lib/cdpmonitor/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cdpmonitor

import (
"encoding/json"
"slices"
"strings"

"github.com/kernel/kernel-images/server/lib/events"
Expand All @@ -10,6 +11,21 @@ import (

func ptrOf[T any](v T) *T { return &v }

// credentialHeaders are never published: Chrome's sanitized HAR default, plus proxy auth.
var credentialHeaders = []string{"authorization", "proxy-authorization", "cookie", "set-cookie"}

// publishedHeaders decodes CDP headers without the credential headers.
func publishedHeaders(raw json.RawMessage) oapi.BrowserHttpHeaders {
var hdrs oapi.BrowserHttpHeaders
_ = json.Unmarshal(raw, &hdrs)
for name := range hdrs {
if slices.Contains(credentialHeaders, strings.ToLower(name)) {
delete(hdrs, name)
}
}
return hdrs
}

// optPtr returns a pointer to s, or nil when s is empty.
func optPtr(s string) *string {
if s == "" {
Expand Down
3 changes: 1 addition & 2 deletions server/lib/cdpmonitor/workers_chrome_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,7 @@ func TestNetworkCaptureFromWorkers(t *testing.T) {
require.NoError(t, json.Unmarshal(requestEvent.Data, &request))
require.Equal(t, worker.targetType, string(request.TargetType))
require.True(t, request.TargetType.Valid())
require.NotNil(t, request.PostData)
require.JSONEq(t, body, *request.PostData)
require.Nil(t, request.PostData, "request bodies are not published")
responseEvent := waitForNetworkURL(t, ec, EventNetworkResponse, url)
var response oapi.BrowserNetworkResponseEventData
require.NoError(t, json.Unmarshal(responseEvent.Data, &response))
Expand Down
Loading