Interceptor Pipeline
Every HTTP/HTTPS request that flows through Ghost doesn’t just get recorded passively. It passes through an interceptor pipeline — an ordered chain of five processing stages that can inspect, modify, tag, block, or persist the traffic. Understanding the pipeline helps you know when and where your breakpoints, rules, scripts, and security analysis take effect.
Think of it like an assembly line in a factory: each station performs one specific job on the item before passing it to the next station. The order matters — a breakpoint needs to pause the original request before any rules modify it, and security analysis needs to run on the final version after all modifications are applied.
Pipeline Architecture
Section titled “Pipeline Architecture”What this diagram shows: The five interceptor stages in order, each with a distinct color. A request enters from the top and passes through each stage sequentially:
-
BreakpointInterceptor (cyan) — Checks if any breakpoint rules match this request. If yes, pauses the flow and waits for the user to inspect/edit it in the UI. If no breakpoint matches, the flow passes through instantly.
-
MapInterceptor (purple) — Checks if any map rules match. Can redirect the request to a different server, rewrite the URL, or return a fake “mock” response without ever contacting the real server.
-
AddonInterceptor (pink) — Runs user-written JavaScript scripts that can modify headers, change the body, add tags, drop the flow, or return custom responses. The most flexible stage.
-
StorageInterceptor (green) — Saves the flow to the SQLite database. At this point the flow becomes visible in the UI. Also triggers a real-time WebSocket event so the frontend updates instantly.
-
SecurityInterceptor (red) — Scans the request and response for security issues: missing headers, insecure cookies, information leakage, etc. Generates findings stored separately in the database.
The Interceptor Interface
Section titled “The Interceptor Interface”For developers: every interceptor implements a simple Go interface with two methods — one for the request phase (before the request is sent to the upstream server) and one for the response phase (after the response comes back):
type Interceptor interface { // Name returns a human-readable identifier for logging and debugging. Name() string
// OnRequest is called after the proxy receives a request from the client, // before forwarding it to the upstream server. // Return nil to continue, or an *Action to modify behavior. OnRequest(ctx context.Context, f *Flow) *Action
// OnResponse is called after the upstream response is received, // before forwarding it to the client. // Not called if OnRequest returned ActionDrop or ActionRespond. OnResponse(ctx context.Context, f *Flow) *Action}Each method returns a pointer to an Action (or nil to continue normally). The Action.Type field tells the pipeline what to do next:
| Action | What Happens | Example |
|---|---|---|
| nil (Continue) | Pass the flow to the next interceptor in the chain. This is the normal case — most interceptors return nil for most flows. | A breakpoint interceptor that has no matching rules returns nil for every flow. |
| ActionDrop | Stop the pipeline immediately. The flow is silently discarded — the client’s connection is closed and no response is sent. | An addon script that blocks requests to ad-tracking domains drops those flows. |
| ActionRespond | Stop the pipeline and send a custom response back to the client, as if the real server had responded. The request is never forwarded to the upstream server. Only valid in OnRequest. | A map rule configured to return a mock {"status": "ok"} response for a health check endpoint. |
Interceptor Details
Section titled “Interceptor Details”1. Breakpoint Interceptor
Section titled “1. Breakpoint Interceptor”What it does: Pauses flows that match your breakpoint rules, giving you a chance to inspect and optionally modify them before they’re forwarded. This is like setting a breakpoint in a code debugger — execution stops, you examine the state, and you decide what to do next.
When it triggers: You create breakpoint rules that match on hostname pattern, URL path pattern, and/or HTTP method. When a flow matches a rule, the interceptor holds the flow in memory (the Go goroutine handling this request blocks on a channel, waiting for your decision).
What you see: A breakpoint notification appears in Ghost’s UI with a full editor showing the request (or response, depending on the rule’s phase). You can:
- Continue — forward the flow as-is
- Modify and continue — edit headers, URL, or body, then forward the modified version
- Drop — discard the flow entirely
Real-time updates: Paused flows are broadcast via WebSocket as flow.breakpoint events, so the UI updates instantly when a breakpoint triggers.
Auto-timeout: If you don’t take any action within 30 seconds, the flow is automatically continued (unmodified) to prevent the proxy from stalling indefinitely.
2. Map Interceptor
Section titled “2. Map Interceptor”What it does: Applies URL mapping rules that can redirect requests to different servers, rewrite URLs, or return mock responses — all without changing the code in the application being tested.
Three types of rules are supported:
| Rule Type | What It Does | Real-World Use Case |
|---|---|---|
| Local (Mock) | Returns a pre-defined response (you specify the status code, headers, and body) without ever contacting the real server | Testing how your app handles errors: create a local rule that returns a 500 Internal Server Error for the payment API, then see if the app shows an appropriate error message |
| Remote (Redirect) | Forwards the request to a different server while preserving the original URL in the flow record | Pointing your staging app at production APIs to test with real data, or routing traffic from one microservice to a local development instance |
| Rewrite | Applies a regex find-and-replace on the URL before forwarding | Changing API version numbers in URLs (/api/v1/ → /api/v2/) to test backward compatibility |
Rules have a priority order — if multiple rules match a flow, the first matching rule (lowest priority number) wins.
3. Addon Interceptor
Section titled “3. Addon Interceptor”What it does: Runs user-defined JavaScript scripts that can do virtually anything to the traffic. This is the most powerful and flexible interceptor — if the built-in rules and breakpoints can’t do what you need, you can write a script.
Here’s an example addon script:
// Tag all API requests and add a debug headerghost.onRequest(function(flow) { if (flow.request.host === "api.example.com") { flow.tag("api"); flow.request.headers["X-Debug"] = "true"; }});
// Log and tag server errorsghost.onResponse(function(flow) { if (flow.response.statusCode >= 500) { flow.tag("error"); ghost.log("Server error: " + flow.request.url); }});This script does two things: it adds a custom header to all requests going to api.example.com (useful for triggering debug mode on the server), and it tags any response with a 500+ status code as “error” so you can quickly filter to see all server failures.
Safety limits: Each addon runs in an isolated JavaScript virtual machine (using the goja engine) with strict resource limits to prevent a buggy script from crashing Ghost:
- Timeout: 5 seconds maximum per script invocation (
execTimeout = 5s) — if your script takes longer, it’s terminated - Max call stack depth: 512 — prevents infinite recursion from crashing the process (this is call depth, not memory)
- Store key limit: 1,024 keys per addon — prevents unbounded memory growth in persistent state
- Dangerous globals removed:
require,process, andglobalThisare stripped — addons have no filesystem, network, or import capabilities - Execution order: When multiple addons are active, they run in priority order (lower number = runs first)
- Thread safety: Each addon’s goja VM is serialized via a mutex — one flow at a time per addon
4. Storage Interceptor
Section titled “4. Storage Interceptor”What it does: Saves the final state of the flow to the SQLite database. This is the point where a flow becomes “real” — it gets a persistent ID, is written to disk, becomes searchable, and appears in the UI.
Specifically, the storage interceptor:
- Assigns a ULID (time-ordered unique ID) if the flow doesn’t already have one
- Sets the session ID so the flow is associated with the currently active capture session
- Inserts into the
flowstable with all request and response data - Updates the FTS5 search index (via database triggers) so the flow’s content is immediately searchable
- Broadcasts a
flow.createdevent via the WebSocket hub, which the frontend receives and uses to add the new flow to the traffic list in real-time
5. Security Interceptor
Section titled “5. Security Interceptor”What it does: Performs passive security analysis on every flow, looking for common misconfigurations and vulnerabilities. “Passive” means it only reads the traffic — it never modifies requests or sends additional traffic.
The security interceptor runs seven detection checks (all defined in security_interceptor.go):
- Plain HTTP detection — Is the request using
http://instead ofhttps://? Traffic over plain HTTP is visible to network eavesdroppers. - URL sensitive data — Does the URL contain passwords, tokens, or API keys in query parameters? These get logged in browser history, server logs, and referrer headers.
- Missing security headers — Does the response include important headers like
Strict-Transport-Security(HSTS),Content-Security-Policy(CSP),X-Frame-Options,X-Content-Type-Options? Missing headers mean the server isn’t using available browser security features. Clickjacking headers are only checked on HTML responses. - CORS misconfigurations — Is the server’s Cross-Origin Resource Sharing policy too permissive? For example, reflecting the request’s
Originheader back inAccess-Control-Allow-Origincombined withAccess-Control-Allow-Credentials: trueis a vulnerability. - Cookie flag issues — Are cookies missing the
Secureflag (sent over unencrypted HTTP),HttpOnlyflag (JavaScript can read them), orSameSiteattribute (CSRF exposure)? - Information leakage headers — Does the response reveal server software versions (like
Server: Apache/2.4.51) or debug headers (X-Debug,X-Powered-By)? Attackers use this information to find known vulnerabilities. - Response body secrets — Does the response body contain patterns matching AWS access keys, private keys, or JWTs? Also checks for stack traces in error responses (4xx/5xx) that reveal internal code structure.
Architecture: Findings are reported via a callback function (OnFinding), not a channel or queue. The callback is responsible for persisting findings and broadcasting them via WebSocket. Each finding includes CWE and OWASP IDs for reference.
Targeted analysis: The interceptor uses an opt-in model — it only analyzes flows whose host matches configured target patterns (glob syntax, e.g., *.hepsiburada.com). If no target hosts are configured, no scanning occurs. This focuses analysis and prevents noise.
Error Flow Handling
Section titled “Error Flow Handling”When something goes wrong — the upstream server can’t be reached, DNS resolution fails, the TLS handshake is rejected — Ghost still creates a flow record. These error flows pass through the full pipeline just like successful flows:
- Noise detection evaluates whether this is a repetitive error pattern
- The storage interceptor saves the error flow to the database (with the error message recorded)
- Security analysis runs on whatever data is available
- A WebSocket event broadcasts the error to the UI
This is important because error flows are often the most interesting during testing — a failed API call tells you something is broken. Ghost ensures you never miss an error, even if the request never completed.
Pipeline Execution — Step by Step
Section titled “Pipeline Execution — Step by Step”This diagram shows the complete lifecycle of a flow through the pipeline, including both the request phase (before contacting the upstream server) and the response phase (after):
What this diagram shows — the complete flow lifecycle:
Request phase (top half):
- The client sends an HTTP request to the proxy
- The proxy passes it to the Breakpoint interceptor. If a breakpoint rule matches, the flow pauses (the
altbox) until the user takes action in the UI. Otherwise, it continues immediately. - The Map Rules interceptor checks for matching rules. If a “local” rule matches, a mock response is sent directly to the client (the
altbox) — the request never reaches the real server. Otherwise, it continues. - The Addon interceptor runs user scripts. Scripts can modify the request, tag it, drop it (the
altbox — flow is silently discarded), or let it continue. - After all request-phase interceptors have run, the proxy forwards the (possibly modified) request to the real upstream server.
Response phase (bottom half): 6. The upstream server responds 7. The response passes through the interceptors again in the same order: Breakpoint (can pause on response), Map Rules (can modify response headers), Addons (can modify response body/headers) 8. The Storage interceptor saves the complete flow (request + response) to the SQLite database and broadcasts a real-time event 9. The Security interceptor analyzes the flow for vulnerabilities and queues any findings for async database storage 10. The proxy sends the response back to the client
The key insight: the pipeline runs twice for each flow — once for the request (before the upstream server is contacted) and once for the response (after the server responds). This gives each interceptor the opportunity to act on both the outgoing request and the incoming response.