Debugger-driven source review
TL;DR: Reading source tells you what should happen; running it under a debugger tells you what does. The single highest-leverage habit in whitebox work is stepping through suspected vulnerable code with attacker-controlled input. Set breakpoints on sinks, trigger the route, inspect taint flow live. Bugs that look uncertain in source become obvious in the debugger.
What it is
Senior auditors don’t audit code by reading — they audit code by running. The debugger is the difference between “this might be exploitable depending on the sanitiser” and “I just watched the unsanitised value reach the sink.” This note is the discipline.
Why it matters
- Framework defaults are often invisible in source. Spring’s
MessageConverter, Rails’ parameter coercion, Express’s body-parser — each can sanitise, normalise, or reject input before your “vulnerable” code runs. A debugger shows what’s actually in the variable. - Reflection/DI/AOP code paths are not greppable. A
@Aspectthat runs before every method, apyenv hook, a Railsbefore_action :sanitize— invisible in the handler, visible in the call stack. - Middleware ordering bugs are runtime-only. The debugger shows you what fires in what order.
- Annotation-driven security (
@PreAuthorize,@UseGuards,before_action) silently fails when misconfigured. The debugger shows whether the guard ran.
Setup per stack
Java / Spring
- IntelliJ remote debug or
mvn spring-boot:runwith-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5005. - Set conditional breakpoints on sinks:
Runtime.exec,ObjectInputStream.readObject,EntityManager.createNativeQuery. - Use “Evaluate expression” to compute payload variants on the fly.
.NET
dnSpyfor decompiled DLLs (closed-source apps), Visual Studio / Rider for source.- Attach to
dotnetor IIS worker; conditional breakpoints onBinaryFormatter.Deserialize,Process.Start,SqlCommandconstructor.
Python
pdb/ipdbinline;debugpyfor remote (python -m debugpy --listen 5678 --wait-for-client).- For Django:
python manage.py runserver+import pdb; pdb.set_trace()at the sink. - VS Code Python debug attach to running process.
Ruby / Rails
binding.pryordebugger(debuggem on Ruby 3.1+) at suspected sink.rails consoleto evaluate payload constructions without re-running the request.byebugfor older codebases.
Node.js
node --inspect-brk+ Chrome DevToolschrome://inspect, or VS Code “Attach to Node”.- Conditional breakpoint on
child_process.exec,eval,vm.runInThisContext. - For NestJS:
npm run start:debugis the standard incantation.
PHP
- Xdebug + VS Code/PhpStorm; set
xdebug.start_with_request=triggerandXDEBUG_TRIGGER=1cookie/header. - Breakpoints on
eval,unserialize,include/require,exec-family.
Go
dlv debug(Delve) ordlv attach. VS Code Go extension wraps it.- Conditional breakpoints on
exec.Command, raw query construction.
Workflow
- Trigger from blackbox first. Confirm a route reaches code by sending a request and watching logs.
- Set sink breakpoints. All suspected sinks in the route’s reachable code.
- Set source breakpoints. Where request data enters (controller method, middleware boundary).
- Send the request. With a “marker” payload that’s distinctive (e.g.,
SENTINEL_QUOTE_123'). - Step over middleware. Note what transforms the input. The marker shows you exactly which transformations apply.
- Watch the sink. Did the marker arrive intact? Partially escaped? Replaced with placeholder?
- Iterate payload. Use “Evaluate expression” or restart with adjusted payload to find the variant that bypasses the transform.
Use cases beyond exploit dev
Confirm authorization
- Set breakpoint inside
@PreAuthorize/canCan/Policymethod. Send a request as a low-priv user. Does it run? What does it return?falseand skipped means no auth fired.
Confirm sanitiser
- Set breakpoint on the sanitiser function. Send distinctive input. Did sanitiser run? With what arguments? Did it modify or just check?
Find hidden code paths
- Breakpoint everywhere; trigger a route; the call stack at each break reveals AOP/decorator/middleware code you didn’t know existed.
Reproduce race conditions
- Breakpoint on the read; freeze; send a parallel write request; observe the read still sees stale state. TOCTOU confirmed.
Pitfalls
- Hot reload / bytecode rewriting (Spring DevTools, Django auto-reload) sometimes loses breakpoints. Pin to debug mode and disable hot reload.
- Optimised builds strip line info. Audit on debug builds.
- Remote debugger over the internet is dangerous — JDWP is unauthenticated by default. Tunnel via SSH.
- Production debug is forbidden by definition; never attach to live customer systems without explicit auth + change window.