Your HAR File Contains Your Session Token

Something is broken, so support asks you to open the browser's developer tools, right-click the network panel and "Save all as HAR". You attach the file to the ticket and get on with your day.

What you attached is a complete recording of that browsing session: every URL with its query string, every request header, every cookie, every form submission, and in most browsers the full body of every response. Including, almost always, the Authorization header carrying your bearer token and the Cookie header carrying your live session.

This is not a hypothetical. It is the normal contents of a normal HAR, and it has produced real incidents — most publicly in late 2023, when support-uploaded HAR files were used to reach the session tokens inside them and pivot into the customer accounts they belonged to.

What is actually in the file

A HAR is JSON, so you can read it. Here is a single entry, trimmed, of the kind a login request produces:

{
  "request": {
    "method": "POST",
    "url": "https://api.example.com/v1/login?api_key=SEKRET123&page=2",
    "headers": [
      { "name": "Authorization", "value": "Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIi..." },
      { "name": "Cookie",        "value": "session=abc123" },
      { "name": "X-Sid",         "value": "internal-session-9931" }
    ],
    "postData": { "text": "{\"password\":\"hunter2\",\"email\":\"[email protected]\"}" }
  },
  "response": {
    "content": { "text": "{\"access_token\":\"eyJhbGciOiJIUzI1NiJ9...\"}" }
  }
}

Five separate credentials in one entry: the API key in the URL, the bearer token, the session cookie, an application-specific session header, and the password in the POST body. Then the response hands back a fresh access token for good measure.

A real capture has hundreds of entries like this.

Why the obvious fix does not work

The intuitive approach is a denylist: strip Authorization, strip Cookie, strip Set-Cookie, done.

Look at the entry above again. X-Sid is not on that list. Nor is it on anyone's list, because it is specific to one application. A denylist can only remove the credentials somebody thought of in advance, and every application in the world is free to invent its own header holding its own session.

So the sanitiser here inverts the default. It keeps an allowlist of header names whose values are known to be safe — the standard protocol vocabulary of content negotiation, caching, CORS, client hints and security policy. Anything not on that list keeps its name and loses its value. An unfamiliar header can no longer carry anything out, and you can still see that the request sent it.

Running the entry above through it produces:

url:        https://api.example.com/v1/login?api_key=REDACTED&page=2

Authorization   [redacted]
Cookie          [redacted]
X-Sid           [redacted]        <-- caught by the allowlist, not by a rule
Content-Type    application/json  <-- recognised, kept
User-Agent      Mozilla/5.0       <-- recognised, kept
X-Request-Id    req-42            <-- deliberately kept

postData:   {"password": "[redacted]", "email": "[email protected]"}
response:   { size: 91, mimeType: "application/json",
              comment: "body removed by PrivConvert" }

Note what survived. page=2 is still there, because it is not a secret and removing it would make the capture harder to read. X-Request-Id is deliberately preserved, along with the other tracing headers — traceparent, X-Amzn-Trace-Id, the B3 family — because correlating a HAR against a server log is one of the main reasons anyone opens one.

The five places a secret hides

Headers are only the first. A thorough pass has to cover:

  • Query strings. ?api_key=, ?token=, and the OAuth callback parameters code and state, which are matched whole rather than as substrings — otherwise country_code would be redacted for containing "code".
  • Cookies, in both the header form and the parsed cookies array a HAR keeps alongside it.
  • POST bodies, where the password usually is.
  • Response bodies, removed entirely. This is where the API hands back tokens and customer records. The size and mimeType stay, so you can still tell whether the body arrived and whether it was what you expected.
  • Page titles. Easy to miss: a single-page app often builds document.title from the current route, so the title can be a URL carrying the query string with it.

On top of the name matching there is a value-shaped check, because a JWT announces itself structurally — three base64url runs beginning eyJ, which is {"al base64'd. A token like that is redacted wherever it appears, whatever the field around it was called.

Verifying it worked

You should not take a sanitiser's word for it, and you do not have to. A HAR is JSON, so the check is a text search for the values you know were in it:

grep -c 'SEKRET123' sanitised.har   # the API key
grep -c 'hunter2'   sanitised.har   # the password
grep -c 'eyJhbGci'  sanitised.har   # any JWT

Three zeros means three secrets gone. Do this on the file you are about to attach, not on a sample — it takes ten seconds and it is the only check that actually proves anything.

If you have already sent one

Sanitising the next one does nothing about the last one. A HAR that went out unredacted should be treated as a disclosed credential:

  • Sign out everywhere, so the session cookies in it stop working.
  • Rotate any API key that appeared in a URL or header.
  • Change the password if a login request was captured.
  • If it went into a ticketing system, ask for the attachment to be deleted — it is now sitting in a support tool, in a search index, and in whatever backups that tool keeps.

Reading a capture without opening it in a browser

Sanitising is one reason to process a HAR. The other is that a large one is genuinely hard to read as JSON, and the question you have is usually "which requests were slow" or "which ones failed". Exporting the request log to a spreadsheet answers that directly: one row per request, with method, URL, status, MIME type, request and response sizes, total time, wait time and server IP.

Those exports run the URLs through the same cleaner as the sanitiser, so exporting a capture cannot become the leak that sanitising it was meant to prevent.

All of this runs in memory and is discarded as soon as your download is sent, which for this particular file type is rather the point. A HAR is the last thing you want sitting on someone else's disk.

HAR tools:
Sanitise HAR HAR to CSV HAR to XLSX JWT Decoder