Security headers check
Which security headers a site sends, which are missing, and how its cookies are flagged; the redirect chain hop by hop; HSTS in detail (how many days, subdomains, preload); and how the site answers a request from another site (CORS), flagging an origin echoed back with credentials. fetch opens the page in a real browser; curl in the session's shell reads the raw headers.
When to use it: You want to review the security headers and cookie flags of your own site.
| Uses | fetch, a shell-only session (browser: false), exec with env |
|---|---|
| Needs | a plan with shell sessions; no model |
| Site | your site; the default is books.toscrape.com. The runner uses its stand-in page (two headers present, a cookie without Secure or SameSite) |
| Output | output/result.json |
| Languages | Node and Python |
The code
The example as it is in its folder. Both versions do the same job and write the same output/, so one result check covers both.
/** * Security headers check: which security headers a site sends, which are missing, and how its cookies are flagged. * fetch opens the page in a real browser (final address, status); curl in the session's shell reads the raw headers. * * npx tsx node/index.ts (BOXLINE_API_KEY; SITE_URL for your own site; a plan with shell sessions) * * Writes output/result.json. */import { mkdirSync, writeFileSync } from "node:fs";import { join } from "node:path";import { Boxline } from "@boxline/sdk";const out = process.env.OUTPUT_DIR ?? "output";const url = process.env.SITE_URL ?? "https://books.toscrape.com/";const bx = new Boxline();const FOREIGN_ORIGIN = "https://cors-check.example";const WANTED: Record<string, string> = { "strict-transport-security": "forces HTTPS on later visits", "content-security-policy": "limits where scripts, styles and frames come from", "x-content-type-options": "stops the browser guessing file types", "referrer-policy": "limits what the Referer header gives away", "permissions-policy": "switches off browser features the site does not use",};// 1. The page in a real browser: where it ends up and with which status.const page = await bx.fetch(url, { format: "text" });console.log(`${page.finalUrl} answered ${page.status} ("${page.title}")`);// 2. The raw headers, from curl in the session's shell (the address goes in as an environment variable, not code).const session = await bx.sessions.create({ browser: false, shell: true, timeout: 300, userMetadata: { example: "security-headers-check" } });console.log(`Session: ${session.id}`);try { // A second request says Origin: another site, to see whether the site lets other sites read its answers (CORS). const curl = 'curl -sS -o /dev/null -D - -L --max-redirs 5 --max-time 30'; const [r, cors] = await Promise.all([ session.exec(`${curl} "$TARGET_URL"`, { env: { TARGET_URL: url } }), session.exec(`${curl} -H "Origin: $ORIGIN" "$TARGET_URL"`, { env: { TARGET_URL: url, ORIGIN: FOREIGN_ORIGIN }, shell: false }), ]); if (r.exitCode !== 0) throw new Error(`curl failed: ${r.stderr.trim()}`); const blocks = r.stdout.trim().split(/\r?\n\r?\n/).filter(Boolean); // Every hop of the redirect chain: its status and where it sends the browser. const redirects = blocks.slice(0, -1).map((b) => ({ status: Number(/^HTTP\/[\d.]+ (\d{3})/.exec(b)?.[1] ?? 0), location: /^location:\s*(.+)$/im.exec(b)?.[1]?.trim() ?? null })); const last = blocks.at(-1)!; // after redirects, the final answer const lines = last.split(/\r?\n/).slice(1); const headers = new Map<string, string>(); const cookies: { name: string; secure: boolean; httpOnly: boolean; sameSite: string }[] = []; for (const line of lines) { const i = line.indexOf(":"); if (i < 0) continue; const name = line.slice(0, i).trim().toLowerCase(); const value = line.slice(i + 1).trim(); if (name === "set-cookie") { cookies.push({ name: value.split("=")[0]!.trim(), secure: /;\s*secure\b/i.test(value), httpOnly: /;\s*httponly\b/i.test(value), sameSite: /;\s*samesite=(\w+)/i.exec(value)?.[1] ?? "not set", }); } else headers.set(name, value); } const present = Object.keys(WANTED).filter((h) => headers.has(h)); const missing = Object.keys(WANTED).filter((h) => !headers.has(h)); const framing = headers.has("x-frame-options") || /frame-ancestors/i.test(headers.get("content-security-policy") ?? ""); if (!framing) missing.push("x-frame-options (or CSP frame-ancestors)"); else present.push(headers.has("x-frame-options") ? "x-frame-options" : "csp frame-ancestors"); // HSTS in detail: how long browsers must stick to HTTPS, and whether subdomains and the preload list are covered. const hstsValue = headers.get("strict-transport-security"); const hsts = hstsValue ? { maxAgeDays: Math.round(Number(/max-age=(\d+)/i.exec(hstsValue)?.[1] ?? 0) / 86400), includeSubDomains: /includesubdomains/i.test(hstsValue), preload: /preload/i.test(hstsValue) } : null; // CORS: what the site answered to a request from another site. const corsHeaders = (cors.stdout.trim().split(/\r?\n\r?\n/).filter(Boolean).at(-1) ?? "").toLowerCase(); const allowOrigin = /^access-control-allow-origin:\s*(.+)$/m.exec(corsHeaders)?.[1]?.trim() ?? null; const allowCredentials = /^access-control-allow-credentials:\s*true/m.test(corsHeaders); const corsRisk = allowOrigin === FOREIGN_ORIGIN.toLowerCase() && allowCredentials ? "it echoes any site's origin and allows credentials: any site can read a signed-in visitor's answers" : allowOrigin === FOREIGN_ORIGIN.toLowerCase() ? "it echoes any site's origin (fine for public data only)" : allowOrigin === "*" ? "open to every site (fine for public data)" : null; for (const hop of redirects) console.log(` redirect ${hop.status} → ${hop.location}`); for (const h of present) console.log(` ok ${h}`); for (const h of missing) console.log(` MISSING ${h}${WANTED[h] ? `: ${WANTED[h]}` : ": stops other sites framing yours (clickjacking)"}`); for (const c of cookies) console.log(` cookie ${c.name}: ${c.secure ? "Secure" : "not Secure"}, ${c.httpOnly ? "HttpOnly" : "readable by scripts"}, SameSite ${c.sameSite}`); if (hsts) console.log(` HSTS: ${hsts.maxAgeDays} days${hsts.includeSubDomains ? ", subdomains" : ""}${hsts.preload ? ", preload" : ""}${hsts.maxAgeDays < 180 ? " (under the 180 days browsers expect)" : ""}`); console.log(` CORS: ${allowOrigin ? `Access-Control-Allow-Origin ${allowOrigin}${allowCredentials ? " with credentials" : ""}` : "not shared with other sites"}${corsRisk ? `: ${corsRisk}` : ""}`); mkdirSync(out, { recursive: true }); writeFileSync(join(out, "result.json"), JSON.stringify({ url, finalUrl: page.finalUrl, status: page.status, redirects, present, missing, cookies, hsts, cors: { allowOrigin, allowCredentials, risk: corsRisk } }, null, 2));} finally { await session.stop();}"""Security headers check: which security headers a site sends, which are missing, and how its cookies are flagged.fetch opens the page in a real browser (final address, status); curl in the session's shell reads the raw headers. python python/main.py (BOXLINE_API_KEY; SITE_URL for your own site; a plan with shell sessions)Writes output/result.json."""import jsonimport osimport refrom pathlib import Pathfrom boxline import Boxlineout = Path(os.environ.get("OUTPUT_DIR", "output"))url = os.environ.get("SITE_URL", "https://books.toscrape.com/")bx = Boxline()FOREIGN_ORIGIN = "https://cors-check.example"WANTED = { "strict-transport-security": "forces HTTPS on later visits", "content-security-policy": "limits where scripts, styles and frames come from", "x-content-type-options": "stops the browser guessing file types", "referrer-policy": "limits what the Referer header gives away", "permissions-policy": "switches off browser features the site does not use",}# 1. The page in a real browser: where it ends up and with which status.page = bx.fetch(url, format="text")print(f'{page["finalUrl"]} answered {page["status"]} ("{page["title"]}")')# 2. The raw headers, from curl in the session's shell (the address goes in as an environment variable, not code).with bx.sessions.create(browser=False, shell=True, timeout=300, user_metadata={"example": "security-headers-check"}) as session: print(f"Session: {session.id}", flush=True) curl = "curl -sS -o /dev/null -D - -L --max-redirs 5 --max-time 30" r = session.exec(f'{curl} "$TARGET_URL"', env={"TARGET_URL": url}) if r["exitCode"] != 0: raise SystemExit(f"curl failed: {r['stderr'].strip()}") # A second request says Origin: another site, to see whether the site lets other sites read its answers (CORS). cors = session.exec(f'{curl} -H "Origin: $ORIGIN" "$TARGET_URL"', env={"TARGET_URL": url, "ORIGIN": FOREIGN_ORIGIN})blocks = [b for b in re.split(r"\r?\n\r?\n", r["stdout"].strip()) if b]# Every hop of the redirect chain: its status and where it sends the browser.redirects = []for b in blocks[:-1]: status = re.match(r"^HTTP/[\d.]+ (\d{3})", b) location = re.search(r"^location:\s*(.+)$", b, re.I | re.M) redirects.append({"status": int(status.group(1)) if status else 0, "location": location.group(1).strip() if location else None})last = blocks[-1] # after redirects, the final answerheaders, cookies = {}, []for line in re.split(r"\r?\n", last)[1:]: name, sep, value = line.partition(":") if not sep: continue name, value = name.strip().lower(), value.strip() if name == "set-cookie": same_site = re.search(r";\s*samesite=(\w+)", value, re.I) cookies.append({ "name": value.split("=")[0].strip(), "secure": bool(re.search(r";\s*secure\b", value, re.I)), "httpOnly": bool(re.search(r";\s*httponly\b", value, re.I)), "sameSite": same_site.group(1) if same_site else "not set", }) else: headers[name] = valuepresent = [h for h in WANTED if h in headers]missing = [h for h in WANTED if h not in headers]if "x-frame-options" in headers or "frame-ancestors" in headers.get("content-security-policy", "").lower(): present.append("x-frame-options" if "x-frame-options" in headers else "csp frame-ancestors")else: missing.append("x-frame-options (or CSP frame-ancestors)")# HSTS in detail: how long browsers must stick to HTTPS, and whether subdomains and the preload list are covered.hsts_value = headers.get("strict-transport-security")hsts = Noneif hsts_value: age = re.search(r"max-age=(\d+)", hsts_value, re.I) hsts = {"maxAgeDays": round(int(age.group(1)) / 86400) if age else 0, "includeSubDomains": "includesubdomains" in hsts_value.lower(), "preload": "preload" in hsts_value.lower()}# CORS: what the site answered to a request from another site.cors_headers = ([b for b in re.split(r"\r?\n\r?\n", cors["stdout"].strip()) if b] or [""])[-1].lower()m = re.search(r"^access-control-allow-origin:\s*(.+)$", cors_headers, re.M)allow_origin = m.group(1).strip() if m else Noneallow_credentials = bool(re.search(r"^access-control-allow-credentials:\s*true", cors_headers, re.M))if allow_origin == FOREIGN_ORIGIN.lower() and allow_credentials: cors_risk = "it echoes any site's origin and allows credentials: any site can read a signed-in visitor's answers"elif allow_origin == FOREIGN_ORIGIN.lower(): cors_risk = "it echoes any site's origin (fine for public data only)"elif allow_origin == "*": cors_risk = "open to every site (fine for public data)"else: cors_risk = Nonefor hop in redirects: print(f" redirect {hop['status']} → {hop['location']}")for h in present: print(f" ok {h}")for h in missing: print(f" MISSING {h}: {WANTED.get(h, 'stops other sites framing yours (clickjacking)')}")for c in cookies: print(f" cookie {c['name']}: {'Secure' if c['secure'] else 'not Secure'}, {'HttpOnly' if c['httpOnly'] else 'readable by scripts'}, SameSite {c['sameSite']}")if hsts: print(f" HSTS: {hsts['maxAgeDays']} days" + (", subdomains" if hsts["includeSubDomains"] else "") + (", preload" if hsts["preload"] else "") + (" (under the 180 days browsers expect)" if hsts["maxAgeDays"] < 180 else ""))shared = f"Access-Control-Allow-Origin {allow_origin}" + (" with credentials" if allow_credentials else "") if allow_origin else "not shared with other sites"print(f" CORS: {shared}" + (f": {cors_risk}" if cors_risk else ""))out.mkdir(parents=True, exist_ok=True)result = {"url": url, "finalUrl": page["finalUrl"], "status": page["status"], "redirects": redirects, "present": present, "missing": missing, "cookies": cookies, "hsts": hsts, "cors": {"allowOrigin": allow_origin, "allowCredentials": allow_credentials, "risk": cors_risk}}(out / "result.json").write_text(json.dumps(result, indent=2))Inputs
Environment variables the example reads:
SITE_URL: the page to check (defaulthttps://books.toscrape.com/).
Run it
Get the examples with git clone https://github.com/Boxline-dev/examples.git and open this one: cd examples/security-headers-check. Put your API key in BOXLINE_API_KEY (and BOXLINE_API_URL for another API), then run either version from the example’s folder. Both write to output/ and stop their sessions when they finish.
# Node 18 or newer, in the example's folderexport BOXLINE_API_KEY=… # your API keynpm install @boxline/sdk tsxnpx tsx node/index.ts# Python 3.9 or newer, in the example's folderexport BOXLINE_API_KEY=… # your API keypip install boxline-sdkpython python/main.pyThen check the result, from the same folder: npx tsx check.ts output. It exits with 0 only when the result is right.
What its check proves
check.ts reads what the example wrote and passes only when the result is right, not merely when the example ran.
On the stand-in, the 301 in front of the page is the redirect chain, HSTS, CSP, Permissions-Policy and X-Frame-Options are missing, the two it sends are present, the cookie is HttpOnly, not Secure, without SameSite, and the CORS answer that echoes any origin with credentials is flagged.
Source
github.com/Boxline-dev/examples/security-headers-check on GitHub, with its README.md, check.ts and .env.example. The examples are open source. See all examples.
These docs describe a beta API. Endpoints, fields and SDK methods may change before general availability; breaking changes will be listed in the changelog.