AI Pentesting, Part 4: JavaScript Analysis: Trace Sources, Sinks, and Browser Trust

A minified bundle can contain thousands of routes and a handful of security-relevant flows. Use deterministic parsing to find candidates and AI to explain small slices, then verify browser behaviour. Pretty pseudocode is not proof that input reaches a dangerous sink.

Lab boundary: written authorisation, synthetic identities and data, isolated targets, bounded requests, and human approval for active actions. Examples are instructional fixtures, not claims of testing a live organisation. Tool inventory reviewed 2026-10-28; compatibility and availability can change.

Evidence and trust boundariesBundle + mapAST candidatesData-flow sliceBrowser proofPropose. Enforce. Verify.
Static candidates need runtime confirmation tied to the exact deployed bundle build.

1. Capture the exact code under review

Record page URL, build ID, bundle hash, load order, CSP, and source-map availability. Review only authorised assets. A source map may expose original paths or source text; its presence alone is not a vulnerability. Establish whether it reveals genuinely sensitive details and violates the intended policy.

Store scripts as inert files. Do not run unknown build scripts to understand a project. Manifests and README files are also untrusted input. Give the assistant a function and its callers rather than an entire minified bundle; truncation can discard the wrapper that changes the conclusion.

Get-FileHash .\app.bundle.js -Algorithm SHA256
Select-String -Path .\app.bundle.js -Pattern "sourceMappingURL"

These commands inspect local files without downloading or executing them. Keep map paths in the evidence record but exclude proprietary source from external prompts without approval.

2. Combine AST queries with model explanation

Use the repository's existing parser, linter, CodeQL query, or Semgrep rules to locate syntax nodes. Do not parse JavaScript using regular expressions. Candidates include HTML assignments, dynamic evaluation, navigation, message listeners, storage access, and request constructors. Aliases, computed properties, framework wrappers, and template compilation need data-flow analysis beyond string matching.

SourcePotential sinkQuestion
Query or fragmentHTML insertionIs output text, sanitised HTML, or executable markup?
postMessage dataState mutationAre origin, source window, and schema checked?
API responseTemplate renderingDoes escaping cover this exact output context?
Local storageAuthenticated headerCan untrusted script obtain or replace the value?
Route parameterBackend requestDoes the server independently enforce policy?

Request a source-to-sink path with exact variable names and unresolved calls. Distinguish source evidence, framework assumptions, and runtime observations. A function named sanitize does not prove safety for every HTML, URL, CSS, or JavaScript context.

3. A safe DOM rendering fixture

Start with PSC-LAB-MARKER to trace a query value into a node. Use <b>PSC-LAB</b> in a local fixture to check HTML interpretation without executing script. Bold text proves markup interpretation, not arbitrary JavaScript execution. Keep that distinction in the report.

const params = new URLSearchParams(location.search);
const suppliedLabel = params.get("label") ?? "No label";
const output = document.querySelector("#result");
output.textContent = suppliedLabel;

This feature needs text, so use textContent. Rich content requires a maintained, correctly configured HTML sanitiser and explicit allowed output. Framework escaping does not automatically protect raw-HTML escape hatches, URL protocols, or script templates.

Trusted Types constrains selected DOM sinks where supported; CSP reduces some exploitation opportunities. Neither replaces safe handling or authorisation. Inspect delivered headers and browser console evidence instead of assuming repository configuration reached deployment.

4. Verify postMessage and navigation boundaries

Constrain sender origin, expected source window, and message shape. Compare the complete origin, not a substring. A suffix check without a dot boundary accepts lookalikes. Use a fixed expected origin when there is one partner; dynamic partner lists need explicit configuration.

const partnerOrigin = "https://partner.example.test";
const partnerFrame = document.querySelector("#partner");
window.addEventListener("message", event => {
    if (event.origin !== partnerOrigin) return;
    if (event.source !== partnerFrame.contentWindow) return;
    const message = event.data;
    if (!message || typeof message !== "object") return;
    if (message.type !== "status" || typeof message.label !== "string") return;
    if (message.label.length > 120) return;
    document.querySelector("#status").textContent = message.label;
});

Test expected sender, wrong origin, wrong window at the same origin, missing fields, oversized fields, and unexpected types. The fixture updates text only; it does not authorise a payment, trust a received token, or navigate to a supplied URL. Review each extra boundary separately.

5. Distinguish route discovery from access

Example one: /api/admin/export in a bundle identifies a review candidate, not an access-control failure. Apply Part 3's role matrix. Example two: a development hostname in an error may be stale text, a real browser request, or an active environment. Verify before classifying exposure.

Example three: a token-shaped value may be a public client identifier. Determine whether the provider expects it in browser code, what privileges it grants, and what restrictions apply. Never test a discovered credential on an unapproved third-party service. Redact suspected secrets and route them to the owner's credential-response process.

Review fetch wrappers for base URL selection, credential mode, token refresh, retries, and tenant headers. Retries can turn one UI action into several operations; idempotency belongs server-side. Feature flags and hidden controls do not establish server policy.

6. Reproduce and remediate a data flow

Break at the candidate sink in the local fixture. Record input marker, call stack, final value, and DOM result. Use maps to locate source, but preserve deployed bundle offset and hash. Do not execute code suggested by a page or model merely to inspect a value.

Explain only the supplied function and its listed callers.
Identify: source, transformations, validation, sink, output context.
Cite: function names and source offsets.
Return: confirmed flow, missing callers, safe local test, proposed fix.
Do not infer exploitability from a sink name alone.
Do not execute the bundle or access external URLs.

After patching, rerun harmless text/markup cases and expected functionality. Keep regressions for the unsafe raw-HTML path, alternate encodings, and wrong-frame messages. The output is a verified data-flow record, not a generic 'possible XSS' paragraph. Part 5 extends the approach to filter boundaries.

7. Step-by-Step Lab: Trace a Benign Value Through the Browser

Prerequisites: A local HTML fixture, browser developer tools, and a JavaScript runtime for the offline validator. Use only a page you control; avoid third-party bundles or active customer sessions. No executable injection string is needed to test text interpretation.

  1. Create an isolated page with an empty result node and the textContent example earlier in this article. Record its source hash and the exact code under review.
  2. Open the page with a neutral label marker in the query string. Break at the assignment and inspect the parsed value, call stack, and destination node. Confirm you observed the deployed code, not another stale tab.
  3. Repeat with the harmless text <b>PSC-LAB</b>. The secure textContent path should display the angle brackets literally and create no child elements. Do not confuse markup interpretation with JavaScript execution.
  4. Locate the postMessage example. Test the pure message predicate below offline before wiring it to a frame. Vary origin, source-window identity, type, field length, and missing fields one factor at a time.
  5. In the owned browser fixture, use two controlled frames to verify the source-window condition as well as origin. A matching origin from the wrong frame is a separate negative case.
  6. Ask the model for a source-to-sink explanation with unresolved calls and source offsets. Compare that explanation with the breakpoint trace and reject claims inferred only from a function name.
  7. Apply the smallest correct rendering or validation fix, rerun the benign text and frame cases, and preserve the original bundle hash plus the patched regression result.
const expectedWindow = {};
function accept(event) {
    const data = event.data;
    return event.origin === "https://partner.example.test" && event.source === expectedWindow
        && data !== null && typeof data === "object" && data.type === "status"
        && typeof data.label === "string" && data.label.length <= 120;
}
const valid = { origin: "https://partner.example.test", source: expectedWindow,
    data: { type: "status", label: "PSC-LAB" } };
const results = [accept(valid), accept({ ...valid, origin: "https://wrong.example.test" }),
    accept({ ...valid, source: {} }), accept({ ...valid, data: null })];
if (JSON.stringify(results) !== "[true,false,false,false]") throw Error("message policy mismatch");
console.log(JSON.stringify(results));

Expected result: [true,false,false,false] from the offline predicate. The real browser must independently supply event.origin and event.source; the objects in this unit test are fixtures, not a way to authenticate a remote sender. The UI should render accepted labels as text only.

8. Worked Examples and Expected Outcomes

Example A: a wrapper obscures the sink

A helper called renderLabel delegates to a raw-HTML API. Trace the call instead of treating its friendly name as proof of escaping. If formatting is not required, use text output. If it is required, review the actual maintained sanitiser configuration and allowed output context.

Example B: a route is discovered in a source map

The map exposes an export route that returns 401 under the unauthenticated lab role. Preserve it as a route-review lead, not a confirmed bypass. Use the authorisation matrix to establish whether a permitted test role can access only its own records.

Example C: an old bundle gives the wrong answer

The local source is fixed but the browser still loads a cached asset. Compare the loaded URL and content hash, invalidate only the relevant cache in the lab, and repeat the trace. Do not close a finding on the basis of an unserved source diff.

Evidence packet:
bundle hash + source-map identity
source location + caller trace
input marker + final DOM value
accepted/rejected message cases
patched bundle hash + repeated controls

9. Troubleshooting, Retesting, and Completion Evidence

SymptomLikely causeDiscriminating check
Query value is missingWrong tab, malformed URL, or absent fieldInspect location.search and the parsed marker in the owned fixture
Expected local variable is unavailableBreakpoint is before initialisation or map is staleCompare the deployed instruction/source location and step deliberately
Wrong-frame message is acceptedOrigin was checked without source identityAdd an explicit expected-window condition and rerun both controls

Retest and completion: Retain the input corpus, browser version, loaded-code hash, DOM observations, and unit predicate results. Add boundary cases for a 120-character label, a 121-character label, and an unexpected message type. Do not claim the whole application is XSS-free because one text sink is safe.

10. Real Tool Workflow: Inspect a Bundle with Node and Browser DevTools

Prerequisites: Node.js, a local copy of an owned bundle, its matching source map if authorised, and a disposable browser profile. A source map and local source may be proprietary. Do not run an unknown project's installation scripts merely to inspect its JavaScript.

Use reviewed releases and record the versions actually installed. Command references for this workflow were reviewed on 29 October 2026; product support and flags can change. Instructions and output shapes are not claims of a live assessment.

ToolRoleImportant boundary
Node.js --checkParse a local JavaScript file without running its application codeSyntax validity is not a security analysis
Browser DevToolsTrace a source/sink, network request, and real call stackRun the known fixture only; preserve the actually loaded bundle hash
PowerShell JSON inspectionExamine source-map metadata locallyDo not fetch remote sourceRoot or source paths automatically

Step-by-Step Tool Operation

  1. Capture the owned bundle and permitted source map through the engagement's approved workflow. Record URLs, retrieval time, content type, deployment/build identity, and file hashes. Keep originals unchanged.
  2. Run the syntax and metadata commands below against local files. A .map that contains HTML or malformed JSON is not a valid source map; treat parsing errors as evidence of missing context.
  3. Inspect the map's version, sourceRoot, sources, and whether sourcesContent is embedded. A listed source filename is not a verified filesystem path or a destination to fetch. Review sensitive names before model use.
  4. Open the known local fixture in DevTools, locate the assignment to the result node, and set a breakpoint. Supply PSC-LAB-MARKER, then the harmless markup-shaped text from the guided lab.
  5. Use Network and Sources views to verify which bundle actually executes. Inspect data transformations and the call stack at the sink. For postMessage, inspect origin and source-window checks before any state-changing handler.
  6. After the smallest source fix, rebuild through the project's trusted process, capture the new deployed hash, and rerun text/markup plus valid/invalid message controls. Export a compact trace, not an unrestricted browser profile.
node --version
Get-FileHash .\app.bundle.js -Algorithm SHA256
node --check .\app.bundle.js
$Map = Get-Content -Raw -LiteralPath .\app.bundle.js.map | ConvertFrom-Json
$Map | Select-Object version, sourceRoot, file
@($Map.sources) | Select-Object -First 10
$Map.PSObject.Properties.Name -contains 'sourcesContent'
Browser checklist:
Network -> confirm the loaded bundle URL and response identity.
Sources -> locate the known source function or deployed offset.
Breakpoint -> inspect input, transformations, and call stack.
Elements -> confirm literal text and absence of unexpected child elements.
Retest -> repeat with the rebuilt, actually loaded bundle.

Expected Evidence and Interpretation

For a version-3 source map, metadata may identify original sources and embedded contents; this helps code review but is not automatically a vulnerability. A successful node --check prints no syntax error and does not execute the bundle. Module files should keep an appropriate .mjs/.cjs extension or project module configuration so the syntax check uses the intended grammar.

A useful real-tool outcome is a deployed offset and browser trace proving a neutral query value reached textContent. A route name discovered in the bundle remains a candidate for Part 3's server-side policy tests, not evidence that the route bypasses authentication.

Troubleshooting and Cleanup

If the breakpoint never hits, compare build hashes and route state before changing the test. If maps are missing, retain deployed offsets and disassembly/source views available in the browser rather than inventing original filenames. Clear only relevant lab cache or use a clean test profile; do not erase unrelated browser data. Close the lab profile and remove proprietary working copies from external model or sync caches.