
Building a Browser Hacking Simulator from Scratch with JavaScript
What the simulator is for and what it is not
I build small browser simulators when I want to reason about security behavior without firing up a full browser farm every time. The point is not pixel-perfect rendering. The point is to model the parts that matter: state, requests, trust boundaries, and how scripted actions move data around.
That makes the simulator handy for three things:
- reproducing browser-driven attack flows
- testing business logic that lives behind the UI
- finding places where the frontend and backend disagree about trust
It is not a replacement for real browser testing. A simulator will miss layout tricks, timing races, iframe quirks, and a lot of JavaScript engine behavior. Treat it as a fast pre-flight check, not a final answer.
Model the browser as a few testable systems
DOM state and user-visible state
The first mistake is to model “the browser” as one blob. That gets messy quickly. I split it into a few systems and keep them boring on purpose.
DOM state is the structured page data: elements, attributes, text, and simple visibility flags. User-visible state is what the simulated user can actually act on. Those two are not always the same. A button can exist in the DOM and still be hidden, disabled, or blocked by an overlay.
A useful simulator tracks both:
- what the page contains
- what the user can see
- what actions are allowed right now
Network requests and response shaping
The other system that matters is the network. In real bugs, the UI often lies. It may show “upgrade complete” while the backend rejects the request, or hide a control while the API still accepts the call.
Your simulator should let you define request handlers and response shapes separately from the page logic. That lets you test cases like:
- UI says “free plan”
- request still includes privileged fields
- backend returns a 403, 200, or partial success
Trust boundaries between page, script, and backend
This is the part I care about most in security work. The browser has at least three trust boundaries:
| Boundary | What it can do | Common failure |
|---|---|---|
| Page content | display text and controls | misleading instructions |
| Page script | mutate state and send requests | unsafe assumptions about content |
| Backend API | authorize and persist changes | trusting client-side checks |
If you blur those boundaries in the simulator, you will miss the bug class you were trying to study.
Build a minimal simulator core in JavaScript
Event queue and action dispatcher
I usually start with an event queue. Every action becomes a deterministic event: load page, click element, submit form, send request, receive response. That gives you a timeline you can inspect later.
class Simulator {
constructor({ pages, api, logger }) {
this.pages = pages;
this.api = api;
this.logger = logger;
this.state = { page: null, dom: {}, network: [] };
this.queue = [];
}
dispatch(action) {
this.queue.push(action);
while (this.queue.length) {
const next = this.queue.shift();
this.logger.push({ type: "action", next });
this.run(next);
}
}
run(action) {
if (action.type === "load") this.load(action.pageId);
if (action.type === "click") this.click(action.selector);
if (action.type === "request") this.request(action);
}
}
Page objects, fixtures, and scenario steps
Page objects should be plain data. Fixtures describe the page, the API response, and the action sequence. That keeps scenarios readable and repeatable.
const fixture = {
page: {
id: "billing",
dom: {
buttonUpgrade: { visible: true, disabled: false }
}
},
steps: [
{ type: "load", pageId: "billing" },
{ type: "click", selector: "buttonUpgrade" }
]
};
Logging state transitions for later inspection
Do not just log final success or failure. Log state transitions. If a simulated attack succeeds, you want to know which state changed first: visible UI, request payload, response, or stored account state.
A good log line includes:
- timestamp or step index
- action name
- changed state
- request or response summary
That makes diffing much easier when a regression appears.
Add attack-style scenarios safely
Clickjacking-style interaction flow
For clickjacking-style tests, I simulate the user's visible intent and the page's actual response. The bug is not “the button exists.” The bug is “the simulator lets an apparently safe click trigger a sensitive action.”
You can model this by separating the visible target from the effective target:
if (ui.overlayBlocksClick) {
logger.push({ type: "blocked", reason: "overlay" });
} else {
api.post("/dangerous-action", { confirmed: true });
}
Prompt-injection-style content confusion
For content confusion tests, the important detail is that text is not instruction by default. The simulator should let hostile content exist inside the page while a script decides whether to treat it as data or a command.
That means the page can contain text like “ignore previous context,” but the simulator must not automatically obey it. If your tool does, you are testing your own parser bug, not the browser.
Authorization mismatch between UI and API
This is where simulator work becomes useful. The UI might hide an admin action, but the API still accepts it if the caller knows the route and payload shape.
Model that directly:
- render a free-account UI
- attempt the privileged action anyway
- assert that the backend rejects it
If the simulator shows success, the backend is broken. If it shows failure, you still need real testing to confirm the client cannot reach the endpoint another way.
Measure outcomes and avoid false confidence
What the simulator can prove
A simulator can prove that your test logic is deterministic, that your security assumptions are explicit, and that your UI and API rules are aligned in the scenarios you defined.
It is good at finding:
- missing backend authorization
- accidental trust in hidden UI state
- request payload mismatches
- broken scenario logic in security tests
What it cannot prove without real browser testing
It cannot prove browser exploitability. It cannot prove that a race is impossible. It cannot prove that CSS, focus changes, iframe behavior, or script execution will behave exactly like Chrome or Firefox.
So I use it as a gate, not a finish line.
Hardening the simulator for repeatable tests
Deterministic seeds and fixed fixtures
If randomness enters the simulator, test results become noisy. Use fixed seeds for generated data and fixed fixtures for network responses. That way, a failure stays reproducible.
Assertions, snapshots, and failure reports
Add assertions at each step:
- expected visible state
- expected request count
- expected response code
- expected account state
Snapshot the simulator state after each scenario. When a test fails, report the last successful transition and the first unexpected one. That is usually enough to locate the broken assumption quickly.
Conclusion
A browser hacking simulator is most useful when it stays small, explicit, and honest about its limits. Model the systems you can control, keep the trust boundaries visible, and use the output to sharpen real browser tests rather than replace them.
If the simulator makes a security issue obvious, good. If it makes a security claim seem settled, be careful. Real browsers still get the final vote.


