Security Interceptor
The Security Interceptor is Ghost’s built-in vulnerability scanner that runs passively on every HTTP response. It doesn’t send any requests or modify any traffic — it simply analyzes what’s already flowing through the proxy and flags potential security issues. Think of it as a security analyst watching over your shoulder, automatically pointing out problems like “that API is sending an AWS key in the response body” or “that session cookie doesn’t have the Secure flag.”
The interceptor ships with 54 passive checks across two files:
security_interceptor.go— 9 core checks (headers, cookies, CORS, secrets, stack traces, JWT reflection)security_checks_extended.go— 45 extended checks organized in 3 tiers (CSP, credit cards, debug pages, open redirects, and more)
Pipeline Position
Section titled “Pipeline Position”Breakpoints → Map Rules → Addons → Storage → Security InterceptorWhy last? The storage interceptor (position 4) saves the flow to SQLite before the security interceptor runs. This means when the security interceptor creates a finding with a flow_id, that flow already exists in the database — the foreign key reference is valid. If security ran before storage, the finding would reference a flow that doesn’t exist yet.
Read-only: The interceptor never modifies traffic. Its OnRequest method is a complete no-op (returns nil immediately). All analysis happens in OnResponse. It never returns ActionDrop or ActionRespond — it always returns nil, letting traffic continue unmodified.
Opt-In Target Scanning
Section titled “Opt-In Target Scanning”The interceptor only scans traffic to hosts matching configured target patterns. If the target list is empty, nothing is scanned — this prevents noise from CDNs, analytics scripts, font services, and other third-party traffic that would produce hundreds of irrelevant findings.
Target patterns use glob syntax (path.Match from Go’s standard library):
*.hepsiburada.com ← matches api.hepsiburada.com, www.hepsiburada.comapi.example.com ← exact matchstaging-*.internal.net ← matches staging-v2.internal.netHost matching strips the port (:443), lowercases both the host and pattern, and checks both glob match and exact string equality.
Targets are configured in ~/.ghost/config.toml under [security] and can be changed at runtime through the settings API — the interceptor uses a sync.RWMutex to safely update the target list while the proxy is running.
Execution Flow
Section titled “Execution Flow”OnResponse() ├─ checkPlainHTTP() ← always (request URL check) ├─ checkURLSensitiveData() ← always (request URL check) ├─ if Response != nil: │ ├─ checkSecurityHeaders() ← core header checks (HSTS, X-Content-Type-Options, clickjacking) │ ├─ checkCORSHeaders() ← wildcard+credentials, reflected origin │ ├─ checkCookieFlags() ← Secure, HttpOnly, SameSite on session cookies │ ├─ checkInfoLeakageHeaders() ← Server version, X-Powered-By │ ├─ if text content ≤ 5MB: │ │ ├─ checkResponseBodySensitiveData() ← AWS keys, API keys, private keys │ │ ├─ checkStackTraces() ← Java, Python, Go, Node, .NET │ │ └─ checkJWTReflection() ← JWT in response + claim reflection │ └─ runExtendedChecks(body, isHTML, isJS) │ ├─ 17 header-based checks (always run) │ ├─ 14 body-based checks (if text body present) │ ├─ 7 HTML-only checks (if content-type contains html) │ └─ 3 JS-only checks (if content-type contains javascript) └─ Persist findings + broadcast via WebSocketCore Checks (9)
Section titled “Core Checks (9)”These run on every matching response and cover the most common web security issues.
1. Plain HTTP Detection (CWE-319)
Section titled “1. Plain HTTP Detection (CWE-319)”What it checks: Whether the request URL starts with http:// (not https://).
Why it matters: HTTP traffic is unencrypted — anyone on the network (including public Wi-Fi, ISPs, or man-in-the-middle attackers) can read the entire request and response, including passwords, tokens, and personal data.
| Field | Value |
|---|---|
| Condition | URL does NOT start with https:// |
| Severity | High |
| Confidence | 0.95 |
| Type | config |
| CWE | CWE-319 (Cleartext Transmission of Sensitive Information) |
| OWASP | A02:2021 (Cryptographic Failures) |
| Evidence | Method + masked URL + “no TLS encryption” |
2. URL Sensitive Data (CWE-598)
Section titled “2. URL Sensitive Data (CWE-598)”What it checks: Whether the URL’s query parameters contain passwords, tokens, or API keys.
Why it matters: URLs end up in browser history, server logs, referrer headers, and proxy logs. Putting secrets in URLs means they’re visible to anyone who can see the URL — which is far more people than should have access to the secret.
Regex pattern:
(?i)[?&](password|passwd|pwd|secret|token|api_key|apikey|access_token|auth|authorization|private_key|client_secret)=([^&]{1,200})This matches 12 parameter names (case-insensitive), capturing values up to 200 characters. Each matched parameter produces a separate finding.
| Field | Value |
|---|---|
| Severity | High |
| Confidence | 0.9 |
| Type | exposure |
| CWE | CWE-598 (Use of GET Request Method With Sensitive Query Strings) |
| OWASP | A02:2021 |
3. Security Headers
Section titled “3. Security Headers”Three missing header checks, only on HTTPS responses (except X-Content-Type-Options which checks all responses):
| Missing Header | Condition | Severity | Confidence | CWE | OWASP |
|---|---|---|---|---|---|
HSTS (Strict-Transport-Security) | URL starts with https:// AND header is empty | Medium | 0.95 | CWE-319 | A05:2021 |
| X-Content-Type-Options | Header is empty (checked on all responses) | Low | 0.95 | CWE-16 | A05:2021 |
| Clickjacking protection | Content-Type contains html or xhtml AND X-Frame-Options is empty AND Content-Security-Policy does NOT contain frame-ancestors | Medium | 0.9 | CWE-1021 | A05:2021 |
4. CORS Issues (CWE-942)
Section titled “4. CORS Issues (CWE-942)”| Issue | Condition | Severity | Confidence |
|---|---|---|---|
| Wildcard with credentials | Access-Control-Allow-Origin is * AND Access-Control-Allow-Credentials equals true | High | 0.95 |
| Reflected origin with credentials | Request has an Origin header AND response echoes that exact origin in Access-Control-Allow-Origin AND Credentials is true | Medium | 0.7 |
5. Cookie Flags
Section titled “5. Cookie Flags”Only checks cookies that look like session cookies — the cookie name must contain one of 16 session-related substrings (session, token, auth, jwt, etc.). Flag detection parses only the attributes portion (after the first ;) to avoid false negatives on cookie names that happen to contain flag-like strings (e.g., a cookie named securesession won’t incorrectly pass the Secure flag check).
| Missing Flag | Severity | Confidence | CWE | OWASP |
|---|---|---|---|---|
| Secure | Medium | 0.85 | CWE-614 | A02:2021 |
| HttpOnly | Medium | 0.85 | CWE-1004 | A02:2021 |
| SameSite | Low | 0.8 | CWE-1275 | A01:2021 |
6. Information Leakage (CWE-200)
Section titled “6. Information Leakage (CWE-200)”| Issue | Condition | Severity | Confidence |
|---|---|---|---|
| Server version | Server header contains a version number pattern (digit.digit) | Info | 0.95 |
| X-Powered-By | Header is present (any value) | Info | 0.95 |
7. Sensitive Data in Response Body
Section titled “7. Sensitive Data in Response Body”Scans text response bodies (up to 5 MB) for hardcoded secrets. Only runs on text content types.
| Pattern | What It Finds | Severity | Confidence | CWE |
|---|---|---|---|---|
AKIA[0-9A-Z]{16} | AWS Access Key IDs | Critical | 0.9 | CWE-200 |
sk-..., ghp_..., gho_..., github_pat_..., xox[bprs]-... | OpenAI, GitHub, Slack tokens | High | 0.85 | CWE-200 |
-----BEGIN ... PRIVATE KEY----- | PEM-encoded private keys | Critical | 0.95 | CWE-321 |
8. Stack Traces (CWE-209)
Section titled “8. Stack Traces (CWE-209)”Only runs on error responses (4xx/5xx). Detects Java, Python, Go, Node.js, and .NET stack traces that reveal internal details.
9. JWT Reflection (CWE-200)
Section titled “9. JWT Reflection (CWE-200)”Three-tier JWT detection:
| Tier | What It Detects | Severity | Confidence |
|---|---|---|---|
| JWT in response | Server embeds JWT tokens in HTML/JS output (e.g., inline <script> variables). Detected regardless of request content. | High | 0.85 |
| Request JWT reflected | The exact token from Authorization header or cookies appears in the response body. | High | 0.95 |
| JWT claims reflected | Decoded claim values (email, name, role) from request JWT appear in response body. | Medium | 0.8 |
JWT payloads are decoded (base64url) and claim values (email, name, sub, role, etc.) are extracted for evidence. This catches the common pattern where servers decode JWTs server-side and embed user info into HTML/JS output.
Extended Checks — Tier 1: High Impact (10 checks)
Section titled “Extended Checks — Tier 1: High Impact (10 checks)”| Check | Severity | CWE | OWASP | Function |
|---|---|---|---|---|
CSP missing or weak — flags missing CSP entirely, unsafe-inline/unsafe-eval in script-src, wildcard *, data: in script-src | Medium/Low | CWE-693 | A05:2021 | checkCSP |
Private IP disclosure — RFC 1918 IPv4, IPv6 link-local fe80::, AWS EC2 internal hostnames in headers + body | Low | CWE-200 | A01:2021 | checkPrivateIPDisclosure |
| Credit card numbers — Visa/Mastercard/Amex/Discover patterns validated with Luhn checksum | High | CWE-359 | A02:2021 | checkCreditCardDisclosure |
Basic auth over HTTP — Authorization: Basic on http:// URLs or WWW-Authenticate: Basic in response over HTTP | High | CWE-522 | A02:2021 | checkBasicAuthHTTP |
Mixed content — HTTP <script>, <link> (CSS), <iframe> resources loaded from HTTPS pages | Medium | CWE-311 | A02:2021 | checkMixedContent |
Missing anti-CSRF tokens — POST <form> without csrf_token, _token, authenticity_token, etc. | Medium | CWE-352 | A01:2021 | checkAntiCSRFTokens |
Directory listing — <title>Index of /, [To Parent Directory], Directory listing for patterns | Medium | CWE-548 | A01:2021 | checkDirectoryListing |
| SQL errors + Framework debug pages — MySQL, PostgreSQL, Oracle, MSSQL, SQLite errors. Django, Laravel, Spring, Express, Rails, Flask, ASP.NET debug pages. | Medium | CWE-209 | A05:2021 | checkSQLErrors + checkFrameworkDebugPages |
Cache-Control missing — HTTPS responses with Set-Cookie or request Authorization header missing Cache-Control: no-store | Medium | CWE-525 | A04:2021 | checkCacheControl |
Open redirect — 3xx responses where a request query parameter value appears in Location header pointing to an external domain | Medium | CWE-601 | A01:2021 | checkOpenRedirect |
Extended Checks — Tier 2: High Value (10 checks)
Section titled “Extended Checks — Tier 2: High Value (10 checks)”| Check | Severity | CWE | OWASP | Function |
|---|---|---|---|---|
Missing Referrer-Policy — absent or set to unsafe-url/no-referrer-when-downgrade on HTML responses | Low | CWE-200 | A05:2021 | checkReferrerPolicy |
Missing Permissions-Policy — absent on HTML responses (also flags deprecated Feature-Policy) | Low | CWE-693 | A05:2021 | checkPermissionsPolicy |
Cross-domain script without SRI — <script src="..."> pointing to different domain without integrity= attribute | Low | CWE-829 | A08:2021 | checkCrossDomainScriptSRI |
Reverse tabnabbing — <a target="_blank"> without rel="noopener" | Medium | CWE-1022 | A05:2021 | checkReverseTabnabbing |
Dangerous JS sinks — eval(, document.write(, .innerHTML =, .outerHTML =, new Function( | Info | CWE-79 | A03:2021 | checkDangerousJSSinks |
| Session ID in URL — JSESSIONID, PHPSESSID, ASPSESSIONID, sid, token_id in query/path | Medium | CWE-598 | A02:2021 | checkSessionIDInURL |
Full path disclosure — Unix paths (/home/, /var/www/, /usr/) and Windows paths (C:\inetpub\, C:\Users\) | Low | CWE-200 | A05:2021 | checkFullPathDisclosure |
Source map exposed — SourceMap/X-SourceMap response header or //# sourceMappingURL= in JS body | Info | CWE-540 | A05:2021 | checkSourceMapHeader + checkSourceMapInBody |
Debug headers — X-Debug-Token, X-ChromeLogger-Data, X-ChromePhp-Data in response | Medium | CWE-200 | A05:2021 | checkDebugHeaders |
| Password in API response — POST request password field value echoed back in the response body | High | CWE-200 | A02:2021 | checkPasswordInResponse |
Extended Checks — Tier 3: Comprehensive (25 checks)
Section titled “Extended Checks — Tier 3: Comprehensive (25 checks)”| Check | Severity | CWE | Function |
|---|---|---|---|
| Email address disclosure | Info | CWE-200 | checkEmailDisclosure |
| Suspicious HTML/JS comments (TODO, FIXME, password, secret, admin, debug) | Info | CWE-615 | checkSuspiciousComments |
| HSTS misconfiguration (short max-age < 1 year, missing includeSubDomains) | Low | CWE-319 | checkHSTSConfig |
| Cookie domain too broad (leading dot on parent domain) | Info | CWE-565 | checkCookieDomainScope |
Password autocomplete enabled (<input type="password"> without autocomplete=off) | Low | CWE-525 | checkPasswordAutocomplete |
| Deprecated security headers (X-XSS-Protection, X-Content-Security-Policy, X-Webkit-CSP) | Info | CWE-16 | checkDeprecatedHeaders |
| X-AspNet-Version / X-AspNetMvc-Version disclosure | Info | CWE-200 | checkAdditionalInfoLeakage |
| X-Backend-Server disclosure | Low | CWE-200 | checkAdditionalInfoLeakage |
GraphQL introspection enabled (__schema, __type in response) | Low | CWE-200 | checkGraphQLIntrospection |
| Sensitive fields in API JSON (password, secret, ssn, credit_card in keys) | Medium | CWE-213 | checkSensitiveJSONFields |
| CORS null origin with credentials | High | CWE-942 | checkCORSNullOrigin |
Java serialization in response (0xACED0005 magic bytes) | High | CWE-502 | checkJavaSerializationObjects |
| SSN / National ID patterns (US SSN format) | High | CWE-359 | checkSSNDisclosure |
| Cleartext password submission (POST with password field over HTTP) | High | CWE-319 | checkCleartextPasswordSubmission |
| User input reflected in response (query param values in body) | Info | CWE-79 | checkUserInputReflected |
| Insecure HTTP methods (TRACE, TRACK) | Low | CWE-16 | checkInsecureHTTPMethod |
| Big redirect body (3xx response > 1KB) | Info | CWE-200 | checkBigRedirectBody |
| Source code disclosure (PHP/ASP/JSP served as text) | High | CWE-540 | checkSourceCodeDisclosure |
| Compromised CDN domains (polyfill.io, bootcss.com, bootcdn.net) | High | CWE-829 | checkCompromisedCDN |
Content Type Gating
Section titled “Content Type Gating”Checks are gated by content type to avoid false positives and unnecessary processing:
| Gate | Checks |
|---|---|
| Always (headers only) | CSP, Referrer-Policy, Permissions-Policy, Cache-Control, Debug Headers, Info Leakage, Deprecated Headers, CORS Null, HSTS Config, Source Map Header, Cookie Domain, Basic Auth HTTP, Session ID URL, Open Redirect, Insecure HTTP Method, Big Redirect, Cleartext Password |
| Any text body | Private IP, Credit Card, SSN, SQL Errors, Framework Debug, Directory Listing, Path Disclosure, Email, Sensitive JSON, GraphQL, Java Serialization, Source Code, Password in Response, User Input Reflected |
| HTML only | Mixed Content, Anti-CSRF, Reverse Tabnabbing, Cross-Domain SRI, Compromised CDN, Password Autocomplete, Suspicious Comments |
| JavaScript only | Dangerous JS Sinks, Source Map in Body, Suspicious Comments |
Flow Tagging
Section titled “Flow Tagging”When the interceptor finds issues, it adds security:<type> tags to the flow so you can find affected flows using GQL search:
| Tag | When Applied |
|---|---|
security:exposure | Sensitive data found in body (keys, credit cards, JWT tokens, SSN) or URL parameters |
security:config | Missing security headers, CORS issues, stack traces, information leakage, plain HTTP, debug pages |
security:session | Missing cookie flags (Secure, HttpOnly, SameSite) |
Tags are deduplicated — if a flow has both a missing security header and a stack trace, it gets one security:config tag, not two.
Deduplication
Section titled “Deduplication”Each finding gets a dedup key in the format: host|type|title (e.g., api.example.com|config|Missing Strict-Transport-Security header). A UNIQUE database index on (session_id, dedup_key) ensures the same issue is only stored once per session.
This is important because the interceptor runs on every response — if your API consistently sends responses without HSTS, you’d get a finding for every single request without deduplication. With dedup, the finding is stored once, and subsequent duplicate findings are silently skipped (INSERT OR IGNORE returns inserted=false).
Async Architecture
Section titled “Async Architecture”The interceptor doesn’t write to the database directly — it emits findings through callbacks that feed into buffered channels, which are drained by background goroutines:
- Interceptor calls
deps.OnFinding(ctx, findingInfo)— a callback function - Callback creates a
SecurityFindingwith a ULID ID, source"passive", status"open", and the current active session ID. Sends to a buffered channel (capacity: 2,048). - Drain goroutine reads from the channel, calls
db.CreateFinding()with a 5-second timeout. If the finding was actually inserted (not a dedup skip), broadcasts afinding.createdWebSocket event.
The same pattern applies to tag updates — the interceptor calls deps.OnTagsUpdated() which sends to a separate buffered channel (also capacity 2,048), drained by another background goroutine.
Both channels use non-blocking sends (select/default) — if the channel is full (the drain goroutine can’t keep up), the finding or tag update is dropped with a warning log. This prevents the proxy pipeline from blocking on slow database writes.
On shutdown, both channels are closed and the drain goroutines flush all remaining entries before the process exits. This happens after proxyServer.Stop() (so no more findings are produced) but before apiServer.Stop() (so WebSocket broadcasts still work).
Design Rules
Section titled “Design Rules”- All regex precompiled to package-level vars. Zero per-call
regexp.MustCompile. TheextractJSONStringFieldhelper uses byte-level search instead of regex entirely. - Body size cap: 5 MB (
maxBodyScanSize). Larger bodies are skipped. - Cookie flag parsing: Checks only the attributes portion (after first
;) to avoid matching flag-like substrings in cookie names. - Credit card validation: Regex patterns match card number formats, then Luhn checksum validates before reporting — reduces false positives significantly.
- JWT decoding: Base64url payload decoded and parsed as JSON. Signature is NOT verified (passive scanner, not auth validator).
- Compromised CDN check: Case-sensitive matching (HTML
srcattributes are virtually always lowercase). Avoids allocating a full body copy.