Why Online Developer Tools Can Leak Your Secrets — and How to Tell
The threat model of pasting API keys and production JSON into web tools, what client-side only really guarantees, and how to verify it yourself in DevTools.
by Dowon Oh
Every backend engineer I know has done it at least once. You are debugging an auth flow at 11pm, you have a JWT in your clipboard, and the fastest way to see what is inside it is to paste it into the first decoder Google returns. Or you have a 400-line production JSON payload that refuses to parse, and a web-based formatter is right there. The paste takes half a second. The question you did not stop to ask is: where did that token just go? I run ToolRunner, a collection of browser-based developer tools, which means I have spent a lot of time on exactly this question — both as the person building the tools and as the person who would never paste a production credential into a site he could not audit. This article covers the actual threat model of web-based tools, what the phrase "client-side only" does and does not guarantee, how to verify a tool's behavior yourself in five minutes with DevTools, and how I architected ToolRunner so that there is no server capable of receiving your input in the first place.
What you are actually pasting
Think about what flows through a typical developer-tool session. A JWT is not just "a token" — its payload routinely carries user IDs, email addresses, role claims, and internal service names, and if it is a live access token, anyone holding it can act as that user until it expires. A database connection string is a hostname, a username, and a password in one line. An API key is a bearer credential with no context needed at all. Production JSON responses carry customer PII, internal endpoint URLs, and feature flags you have not announced. Even a cron expression or a SQL query can reveal schema names and business logic.
The OWASP Secrets Management Cheat Sheet treats every one of these as a secret whose exposure surface should be minimized — and a third-party web page you have never audited is about as large as an exposure surface gets. The failure mode is not hypothetical malice. A tool site does not need to be evil to burn you:
- It processes on a server. Many "online formatters" POST your input to a backend, format it there, and return the result. Your secret is now in their application logs, their load balancer logs, and whatever log aggregation service they forward to — with retention policies you will never see.
- It logs by accident. Query strings, error reporters, and analytics events routinely capture more than the developer intended. A crash-reporting SDK that serializes application state can ship your pasted input to a third party as a side effect of a bug.
- It changes hands. Browser-tool sites get sold, abandoned, and re-monetized. The domain you audited last year may run different JavaScript today. A compromised third-party script — the supply-chain scenario — turns a previously honest page into an exfiltration point overnight.
- It is remembered in places you forgot. If the tool stores your input in the URL for shareability, that URL lands in your browser history, your sync backend, and the access logs of any server the link is later requested from.
None of this requires attributing bad intent to any specific site. It is the structural risk of handing sensitive data to code you have not read, running in a context you do not control.
What "client-side only" actually guarantees
"Client-side only" is the standard reassurance, and it is worth being precise about what it means. The strong version of the claim is: the JavaScript that processes your input runs in your browser tab, and no code path transmits that input over the network. If that is true, the tool operator cannot leak your secret because they never receive it — there is nothing in their logs, nothing in their database, nothing to subpoena, nothing to breach.
But the phrase, as marketing copy, guarantees none of that. A page can format your JSON locally and fire a fetch() to an analytics endpoint with the payload attached. It can process locally today and quietly add a server round-trip next quarter. The words on the landing page are a claim, not a mechanism. Three mechanisms actually constrain what a page can do:
- The code that ships. If the page's JavaScript contains no call that transmits your input — no
fetch, noXMLHttpRequest, nosendBeacon, no WebSocket carrying it — the input stays in the tab. This is verifiable but tedious, especially against minified bundles. - The Content-Security-Policy. A CSP
connect-srcdirective is enforced by the browser, not promised by the operator: it is an allowlist of origins the page's scripts are permitted to open connections to. Ifconnect-srcnames three origins, then even a compromised script on that page cannot POST your token toattacker.example— the browser refuses the connection before a packet leaves. I wrote about how this works in What is CSP?, and it is the closest thing the web platform has to a technical enforcement of "client-side only." - Your own observation. The Network tab shows every request the page makes, and it does not lie. This is the great equalizer: you do not need to trust the operator's claim or read their bundle, because you can watch the wire yourself.
The honest framing, then, is that "client-side only" is trustworthy exactly to the degree that it is verifiable — and a well-architected tool site should make verification easy rather than asking you to take its word.
What ToolRunner actually loads — including the parts that phone home
Here is where I owe you full honesty about my own site, because "we send nothing anywhere" would be a lie, and the difference between the truth and that lie is precisely the point of this article.
ToolRunner loads two categories of third-party script. First, Google Analytics 4: the app initializes GA with send_page_view disabled — this matters, because gtag's automatic pageview reports the full URL including the query string, and on this site the query string is where tools sync your input. Instead, on each route change the app sends a manual pageview carrying the URL path — location.pathname, literally, in the source — with the page location explicitly overridden to the query-stripped URL. That means Google learns that some visitor opened /jwt-decoder at 11pm. It does not receive the query string, and it does not receive anything you type or paste, because no code path hands tool input to the analytics layer. Second, Google AdSense: the ad script loads from pagead2.googlesyndication.com and does what ad scripts do — fetches and renders ads, sets its own cookies, and observes that a page in its network was viewed. That is real third-party telemetry, it is how a free tool site pays its hosting bill, and you should weigh it like any other tracking. What neither script can see is the content of your work: your JSON, your tokens, your cron expressions. Those live in React component state inside your tab.
The architecture backs this up in three enforceable ways rather than by promise:
- There is no backend. ToolRunner is a statically hosted React app. Every tool — the JSON Viewer, the JWT Decoder, the HTTP Header Analyzer — does its parsing, decoding, and validation with in-browser JavaScript. There is no API server to POST your input to, which means there is no server-side log for it to end up in. You cannot leak to infrastructure that does not exist.
- CSP closes the exfiltration paths. The site ships a
Content-Security-Policywhoseconnect-srcallowlists only the site's own origin plus the Google Analytics, Google ads, and Cloudflare Insights endpoints. Even if a script on the page were compromised, the browser would block any attempt to open a connection to an origin outside that short list. You can read the full header yourself with the HTTP Header Analyzer pointed attoolrunner.dev, and the companion article on HTTP security headers explains each directive. - The one nuance: URL-synced state. Several tools sync your input into the page's query string so you can bookmark or share your work. This happens via
history.replaceState, which is a purely local browser API — updating the address bar sends nothing over the network. But be aware of what URLs are: if you reload a page with input in the query string, that query string travels to the CDN in the request line, and if you share the link, the recipient gets your input by design. This is why I would still tell you: shareable URLs are for cron expressions and sample JSON, not for live credentials. A secret in a URL is a secret in your history.
That last point matters to me more than the reassuring parts. A tool site that tells you only the flattering half of its data-flow story has already failed the trust test.
Verify it yourself: a five-minute DevTools audit
Do not take my word for any of the above — the entire premise here is that you should not have to. This is the audit I run against any web tool before pasting anything I care about, and it works on ToolRunner exactly as it works on anyone else's site.
- Open the Network tab first. Press F12, switch to Network, check "Preserve log," and then load the tool page. You will see the initial document, JS/CSS bundles, and — on ad-supported sites — analytics and ad requests. That is the baseline.
- Paste dummy data and watch the wire. Type a fake token or junk JSON and use the tool. Watch for new requests. Filter by
Fetch/XHRandWSto cut the noise. Click any request that appears and inspect its payload — a pageview beacon carrying a path is very different from a POST carrying your input. Use the search feature of the Network panel to search all request bodies for a distinctive string you pasted; if it appears nowhere, it was not transmitted. - Go offline and use the tool again. In DevTools, set Network throttling to "Offline" (or turn off Wi-Fi) after the page has loaded, then run the tool on new input. A genuinely client-side tool keeps working, because everything it needs already arrived. A tool that silently depends on a server fails immediately. This is the single most convincing test, and it takes ten seconds.
- Read the CSP. Check the response headers on the main document (Network tab → click the document → Headers) and find
Content-Security-Policy. Look atconnect-src: those are the only origins scripts on this page can talk to. A short, explicit allowlist is a structural commitment; a missing CSP or aconnect-src *means the page reserves the right to send data anywhere. - Check where your input lingers. Look at the address bar after typing — did your input appear in the URL? Check Application → Local Storage for saved copies. Neither is a network leak, but both are persistence you should know about before pasting a credential on a shared machine.
- When the stakes are real, do not paste at all. For a live production credential, the correct tool is a local one:
jqfor JSON, a two-line script for Base64 or JWT payloads. Browser tools — mine included — are for the 95% of inputs that are not secrets, and for the 5% that are, rotate first or stay on your own machine.
Steps 2 and 3 together are close to a proof: if no request carried your input while online, and the tool still works with the network physically unavailable, then the claim "your data never leaves the tab" has been demonstrated rather than asserted. That is the standard I think every browser-based tool should invite its users to hold it to — and it is the reason ToolRunner's architecture has no server in the data path at all: the strongest privacy guarantee is the one where there is nothing to trust.
Sources
OWASP
Mozilla MDN
Further reading
- Complete Guide to HTTP Security Headers — the full tour of the response headers referenced in the audit above.
- What is Content-Security-Policy (CSP)? A Practical Guide — a deeper look at the directive that makes "client-side only" enforceable.
- HSTS Explained — why the transport layer under all of this matters too.