
Testing Five Classic JavaScript Weaknesses in Modern Single-Page Apps
Modern SPAs can hide a lot of risk behind a polished interface. The app feels self-contained, but the same old bugs still show up once you trace data from browser state to backend response.
I usually start with five checks: where untrusted data enters the DOM, where the client pretends to make authorization decisions, where HTML is rendered unsafely, where navigation can be abused, and where secrets leak into places they should not be.
Why old JavaScript bugs still matter in SPAs
The frontend does not define trust. It only decides what to show and what to request.
That matters because SPAs encourage patterns that look safe at a glance:
- route guards that only exist in JavaScript
- state stored in
localStorageor in-memory stores - API responses rendered directly into components
- redirects assembled from query parameters
- debug logs left in client bundles
The bug is usually not in the framework. It is in the assumption that browser code can enforce policy by itself.
The five weaknesses to test first
DOM-based XSS in client-side routing
Hash fragments, query strings, and route params often get copied into the page. If the app uses innerHTML, template literals, or unsafe markdown rendering, that input can turn into script execution.
Look for code that turns route data into markup without encoding. A classic smell is “we sanitize later” when later never happens.
Trusting client state for authorization
A lot of SPAs hide buttons based on role or plan type. That is fine for UX, but it is not authorization.
If the browser can flip a flag like isAdmin, plan=premium, or canExport=true, check whether the backend still enforces the same rule. If it does not, the UI was only decoration.
Unsafe HTML rendering from API data
APIs often return rich text, comments, tickets, emails, or CMS content. If the app renders that content as HTML, the safety of the page depends on the sanitizer.
I test whether the app strips dangerous attributes, blocks scriptable URLs, and prevents event handler injection. Even if the backend considers the content trusted, the frontend still has to treat it as hostile until proven otherwise.
Open redirects and navigation abuse
Redirect parameters are not always a direct compromise, but they are a common pivot point.
Check whether the app accepts a next, returnUrl, or redirect value and sends the browser to arbitrary destinations. That can be used for phishing, login confusion, or token leakage if the app appends sensitive state to URLs.
Leaky secrets in bundles, storage, and logs
Secrets should not be in client bundles. That sounds obvious, yet I still find API keys, feature flags, internal endpoints, and debug metadata exposed in build output.
Also inspect localStorage, sessionStorage, IndexedDB, and console logs. If a token is readable by JavaScript, any injected script can read it too.
How to reproduce each issue safely
Browser devtools checks
Start in DevTools and trace the data flow:
- Inspect the DOM after navigation.
- Watch how route parameters are inserted.
- Search for
innerHTML,dangerouslySetInnerHTML, and custom sanitizers. - Review event handlers and network-triggered state changes.
If a route parameter affects rendering, try harmless strings first. You are looking for whether the app encodes by default, not for destructive payloads.
Network and storage inspection
Open the Network tab and verify which checks happen server-side.
| Layer | What to inspect | What should happen |
|---|---|---|
| UI | Role-based buttons | Hidden controls stay hidden |
| API | Privileged actions | Server rejects unauthorized calls |
| Storage | Tokens and profile state | No sensitive secrets in readable storage |
| Redirects | Return URLs | Only allow approved destinations |
Then inspect storage. If a long-lived token sits in localStorage, treat it as readable by any script running on the origin.
Minimal proof-of-concept snippets
For DOM insertion, I look for patterns like this:
const value = new URL(location.href).searchParams.get("q");
result.innerHTML = value;
For safer rendering, this should be text-only:
result.textContent = value;
For authorization, client state should never be the final decision:
// UI hint only
if (user.role === "admin") showAdminButton();
// real check belongs on the server
These examples are small on purpose. In audits, the bug is usually visible in one line once you know where to look.
What good defenses look like
Server-side authorization checks
The backend must verify access on every protected action. Do not trust hidden buttons, route guards, or client-side flags.
If a free account can still call the privileged endpoint, the frontend did not fail. The server did.
Output encoding and safe rendering
Render text as text unless you have a very specific reason not to. When HTML is required, use a sanitizer with an explicit allowlist and test it against your actual content types.
Also review markdown pipelines, rich-text editors, and CMS output separately. Each one has its own failure mode.
Token handling and storage choices
Prefer short-lived tokens and avoid putting long-term credentials in readable browser storage. If you must store session material client-side, understand that any XSS event can reach it.
A safer pattern is to keep the most sensitive material off the page entirely and rely on server-managed sessions where practical.
A short testing checklist for your next SPA audit
- Check route params for unsafe DOM insertion.
- Confirm the server enforces authorization, not just the UI.
- Inspect API-driven rich text for unsafe HTML rendering.
- Review redirect parameters for arbitrary destinations.
- Search bundles and storage for secrets, tokens, and debug data.
- Verify that any sanitizer is actually used on every rich content path.
- Treat client-side state as untrusted input.
Conclusion
SPAs do not create new classes of weakness so much as they make old ones easier to miss. The browser is fast, stateful, and forgiving, which is exactly why a small trust mistake can sit unnoticed until it becomes an exploit.
If you test the five areas above first, you will catch a lot of real problems quickly. The useful habit is simple: follow the data, not the UI.


