Skip to content

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)
Breakpoints → Map Rules → Addons → Storage → Security Interceptor

Why 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.

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.com
api.example.com ← exact match
staging-*.internal.net ← matches staging-v2.internal.net

Host 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.

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 WebSocket

These run on every matching response and cover the most common web security issues.

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.

FieldValue
ConditionURL does NOT start with https://
SeverityHigh
Confidence0.95
Typeconfig
CWECWE-319 (Cleartext Transmission of Sensitive Information)
OWASPA02:2021 (Cryptographic Failures)
EvidenceMethod + masked URL + “no TLS encryption”

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.

FieldValue
SeverityHigh
Confidence0.9
Typeexposure
CWECWE-598 (Use of GET Request Method With Sensitive Query Strings)
OWASPA02:2021

Three missing header checks, only on HTTPS responses (except X-Content-Type-Options which checks all responses):

Missing HeaderConditionSeverityConfidenceCWEOWASP
HSTS (Strict-Transport-Security)URL starts with https:// AND header is emptyMedium0.95CWE-319A05:2021
X-Content-Type-OptionsHeader is empty (checked on all responses)Low0.95CWE-16A05:2021
Clickjacking protectionContent-Type contains html or xhtml AND X-Frame-Options is empty AND Content-Security-Policy does NOT contain frame-ancestorsMedium0.9CWE-1021A05:2021
IssueConditionSeverityConfidence
Wildcard with credentialsAccess-Control-Allow-Origin is * AND Access-Control-Allow-Credentials equals trueHigh0.95
Reflected origin with credentialsRequest has an Origin header AND response echoes that exact origin in Access-Control-Allow-Origin AND Credentials is trueMedium0.7

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 FlagSeverityConfidenceCWEOWASP
SecureMedium0.85CWE-614A02:2021
HttpOnlyMedium0.85CWE-1004A02:2021
SameSiteLow0.8CWE-1275A01:2021
IssueConditionSeverityConfidence
Server versionServer header contains a version number pattern (digit.digit)Info0.95
X-Powered-ByHeader is present (any value)Info0.95

Scans text response bodies (up to 5 MB) for hardcoded secrets. Only runs on text content types.

PatternWhat It FindsSeverityConfidenceCWE
AKIA[0-9A-Z]{16}AWS Access Key IDsCritical0.9CWE-200
sk-..., ghp_..., gho_..., github_pat_..., xox[bprs]-...OpenAI, GitHub, Slack tokensHigh0.85CWE-200
-----BEGIN ... PRIVATE KEY-----PEM-encoded private keysCritical0.95CWE-321

Only runs on error responses (4xx/5xx). Detects Java, Python, Go, Node.js, and .NET stack traces that reveal internal details.

Three-tier JWT detection:

TierWhat It DetectsSeverityConfidence
JWT in responseServer embeds JWT tokens in HTML/JS output (e.g., inline <script> variables). Detected regardless of request content.High0.85
Request JWT reflectedThe exact token from Authorization header or cookies appears in the response body.High0.95
JWT claims reflectedDecoded claim values (email, name, role) from request JWT appear in response body.Medium0.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)”
CheckSeverityCWEOWASPFunction
CSP missing or weak — flags missing CSP entirely, unsafe-inline/unsafe-eval in script-src, wildcard *, data: in script-srcMedium/LowCWE-693A05:2021checkCSP
Private IP disclosure — RFC 1918 IPv4, IPv6 link-local fe80::, AWS EC2 internal hostnames in headers + bodyLowCWE-200A01:2021checkPrivateIPDisclosure
Credit card numbers — Visa/Mastercard/Amex/Discover patterns validated with Luhn checksumHighCWE-359A02:2021checkCreditCardDisclosure
Basic auth over HTTPAuthorization: Basic on http:// URLs or WWW-Authenticate: Basic in response over HTTPHighCWE-522A02:2021checkBasicAuthHTTP
Mixed content — HTTP <script>, <link> (CSS), <iframe> resources loaded from HTTPS pagesMediumCWE-311A02:2021checkMixedContent
Missing anti-CSRF tokens — POST <form> without csrf_token, _token, authenticity_token, etc.MediumCWE-352A01:2021checkAntiCSRFTokens
Directory listing<title>Index of /, [To Parent Directory], Directory listing for patternsMediumCWE-548A01:2021checkDirectoryListing
SQL errors + Framework debug pages — MySQL, PostgreSQL, Oracle, MSSQL, SQLite errors. Django, Laravel, Spring, Express, Rails, Flask, ASP.NET debug pages.MediumCWE-209A05:2021checkSQLErrors + checkFrameworkDebugPages
Cache-Control missing — HTTPS responses with Set-Cookie or request Authorization header missing Cache-Control: no-storeMediumCWE-525A04:2021checkCacheControl
Open redirect — 3xx responses where a request query parameter value appears in Location header pointing to an external domainMediumCWE-601A01:2021checkOpenRedirect

Extended Checks — Tier 2: High Value (10 checks)

Section titled “Extended Checks — Tier 2: High Value (10 checks)”
CheckSeverityCWEOWASPFunction
Missing Referrer-Policy — absent or set to unsafe-url/no-referrer-when-downgrade on HTML responsesLowCWE-200A05:2021checkReferrerPolicy
Missing Permissions-Policy — absent on HTML responses (also flags deprecated Feature-Policy)LowCWE-693A05:2021checkPermissionsPolicy
Cross-domain script without SRI<script src="..."> pointing to different domain without integrity= attributeLowCWE-829A08:2021checkCrossDomainScriptSRI
Reverse tabnabbing<a target="_blank"> without rel="noopener"MediumCWE-1022A05:2021checkReverseTabnabbing
Dangerous JS sinkseval(, document.write(, .innerHTML =, .outerHTML =, new Function(InfoCWE-79A03:2021checkDangerousJSSinks
Session ID in URL — JSESSIONID, PHPSESSID, ASPSESSIONID, sid, token_id in query/pathMediumCWE-598A02:2021checkSessionIDInURL
Full path disclosure — Unix paths (/home/, /var/www/, /usr/) and Windows paths (C:\inetpub\, C:\Users\)LowCWE-200A05:2021checkFullPathDisclosure
Source map exposedSourceMap/X-SourceMap response header or //# sourceMappingURL= in JS bodyInfoCWE-540A05:2021checkSourceMapHeader + checkSourceMapInBody
Debug headersX-Debug-Token, X-ChromeLogger-Data, X-ChromePhp-Data in responseMediumCWE-200A05:2021checkDebugHeaders
Password in API response — POST request password field value echoed back in the response bodyHighCWE-200A02:2021checkPasswordInResponse

Extended Checks — Tier 3: Comprehensive (25 checks)

Section titled “Extended Checks — Tier 3: Comprehensive (25 checks)”
CheckSeverityCWEFunction
Email address disclosureInfoCWE-200checkEmailDisclosure
Suspicious HTML/JS comments (TODO, FIXME, password, secret, admin, debug)InfoCWE-615checkSuspiciousComments
HSTS misconfiguration (short max-age < 1 year, missing includeSubDomains)LowCWE-319checkHSTSConfig
Cookie domain too broad (leading dot on parent domain)InfoCWE-565checkCookieDomainScope
Password autocomplete enabled (<input type="password"> without autocomplete=off)LowCWE-525checkPasswordAutocomplete
Deprecated security headers (X-XSS-Protection, X-Content-Security-Policy, X-Webkit-CSP)InfoCWE-16checkDeprecatedHeaders
X-AspNet-Version / X-AspNetMvc-Version disclosureInfoCWE-200checkAdditionalInfoLeakage
X-Backend-Server disclosureLowCWE-200checkAdditionalInfoLeakage
GraphQL introspection enabled (__schema, __type in response)LowCWE-200checkGraphQLIntrospection
Sensitive fields in API JSON (password, secret, ssn, credit_card in keys)MediumCWE-213checkSensitiveJSONFields
CORS null origin with credentialsHighCWE-942checkCORSNullOrigin
Java serialization in response (0xACED0005 magic bytes)HighCWE-502checkJavaSerializationObjects
SSN / National ID patterns (US SSN format)HighCWE-359checkSSNDisclosure
Cleartext password submission (POST with password field over HTTP)HighCWE-319checkCleartextPasswordSubmission
User input reflected in response (query param values in body)InfoCWE-79checkUserInputReflected
Insecure HTTP methods (TRACE, TRACK)LowCWE-16checkInsecureHTTPMethod
Big redirect body (3xx response > 1KB)InfoCWE-200checkBigRedirectBody
Source code disclosure (PHP/ASP/JSP served as text)HighCWE-540checkSourceCodeDisclosure
Compromised CDN domains (polyfill.io, bootcss.com, bootcdn.net)HighCWE-829checkCompromisedCDN

Checks are gated by content type to avoid false positives and unnecessary processing:

GateChecks
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 bodyPrivate 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 onlyMixed Content, Anti-CSRF, Reverse Tabnabbing, Cross-Domain SRI, Compromised CDN, Password Autocomplete, Suspicious Comments
JavaScript onlyDangerous JS Sinks, Source Map in Body, Suspicious Comments

When the interceptor finds issues, it adds security:<type> tags to the flow so you can find affected flows using GQL search:

TagWhen Applied
security:exposureSensitive data found in body (keys, credit cards, JWT tokens, SSN) or URL parameters
security:configMissing security headers, CORS issues, stack traces, information leakage, plain HTTP, debug pages
security:sessionMissing 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.

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).

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:

  1. Interceptor calls deps.OnFinding(ctx, findingInfo) — a callback function
  2. Callback creates a SecurityFinding with a ULID ID, source "passive", status "open", and the current active session ID. Sends to a buffered channel (capacity: 2,048).
  3. 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 a finding.created WebSocket 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).

  • All regex precompiled to package-level vars. Zero per-call regexp.MustCompile. The extractJSONStringField helper 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 src attributes are virtually always lowercase). Avoids allocating a full body copy.