Skip to content

Security Headers Analyzer — C++ source

Paste HTTP response headers to analyze security posture. Checks CSP, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, and Permissions-Policy.

This is the C++ implementation — the same logic the interactive tool runs, in a shareable, citable form.

// HTTP security-header analysis.
//
// Language: C++17 (standard library only)
// Ported from src/lib/security-headers.ts (the canonical TypeScript
// implementation). display source — part of CosmoDev's polyglot tool pages.
//
// Parses a raw response-header block (the "key: value" lines copied from
// DevTools' Network panel), grades each of 8 security headers A/B/C/F, and
// rolls a weighted overall score. Header names match case-insensitively,
// folded (indented) continuation lines join their parent value, and duplicate
// names join with ", ". Malformed lines and the HTTP status line are ignored.
// Never throws.

#include <algorithm>
#include <cctype>
#include <map>
#include <cmath>
#include <string>
#include <vector>

namespace sec_headers {

enum class Grade { A, B, C, F };

static const char* gradeName(Grade g) {
  switch (g) {
    case Grade::A: return "A";
    case Grade::B: return "B";
    case Grade::C: return "C";
    case Grade::F: return "F";
  }
  return "?";
}

/** One security header as analyzed: the parsed value plus its grade and guidance. */
struct ParsedHeader {
  std::string name;   ///< canonical name, e.g. "Strict-Transport-Security"
  std::string value;  ///< raw value as parsed; empty when the header is absent
  bool present = false;
  Grade grade = Grade::C;
  std::string explanation;
  std::string recommendation;
};

struct HeaderSpec {
  const char* name;
  const char* description;
  double weight;
};

/** The 8 security headers this analyzer grades, in display order. */
const std::vector<HeaderSpec>& securityHeaders() {
  static const std::vector<HeaderSpec> HEADERS = {
    {"Strict-Transport-Security",
     "HSTS tells the browser 'only ever reach this site over HTTPS' for max-age seconds. It defeats SSL-strip attacks, which downgrade the very first plain-HTTP visit. includeSubDomains extends the guarantee to every subdomain; preload lets the site join the browser-built HSTS preload list so even the first visit is HTTPS.",
     1.5},
    {"Content-Security-Policy",
     "CSP is the strongest anti-XSS control available in a header: it declares which sources scripts, styles, and other resources may load from. 'unsafe-inline' and 'unsafe-eval' punch holes straight through it - they re-allow inline injection and string-to-code evaluation. A default-src directive is the baseline so resource types you forgot to list inherit a restrictive fallback instead of the browser's permissive one.",
     2},
    {"X-Frame-Options",
     "Stops other sites from embedding this page in an iframe, which is the basis of clickjacking: an attacker overlays invisible UI on your framed page and harvests the victim's clicks. DENY blocks all framing; SAMEORIGIN allows only your own origin. The modern replacement is the CSP frame-ancestors directive.",
     1},
    {"X-Content-Type-Options",
     "With the value nosniff, the browser must respect the declared Content-Type instead of 'helpfully' sniffing the payload. Sniffing turns an uploaded text file into executable JavaScript when its bytes look script-shaped - the classic content-type-confusion attack.",
     1},
    {"Referrer-Policy",
     "Controls how much of the URL the browser leaks in the Referer header when the user navigates to another site. Full URLs can carry IDs, tokens, or search queries to third parties. strict-origin-when-cross-origin sends only the origin cross-origin (the browser default since 2020); no-referrer and same-origin leak even less. unsafe-url sends the full URL everywhere, including on plain-HTTP requests.",
     1},
    {"Permissions-Policy",
     "Declares which browser features (camera, microphone, geolocation, payment, USB...) the page and its embedded iframes may use. An empty allowlist - feature=() - switches the feature off entirely; an =* wildcard re-allows it everywhere, defeating the point of declaring it.",
     1},
    {"X-XSS-Protection",
     "The 2010-era XSS Auditor: browsers inspected reflected input in the page and blocked obviously injected scripts. Chrome removed it in 2019 (its heuristics introduced cross-site leaks of their own) and every modern browser now ignores it. The header is deprecated - CSP is the real defense. Sites either omit it or send '0' to switch old browsers off.",
     0.5},
    {"Cross-Origin-Opener-Policy",
     "Isolates the page's window handle from other origins. Without COOP, a site that opened your page in a popup (or was opened by it) keeps a JS reference to it, enabling some cross-window attacks and Spectre-class side channels. same-origin cuts that shared handle; unsafe-none restores the old shared browsing context.",
     1},
  };
  return HEADERS;
}

/** The full analysis result: per-header rows, the overall grade, and fix list. */
struct HeaderAnalysis {
  std::vector<ParsedHeader> headers;
  Grade grade;
  int score = 0; ///< 0-100 weighted average of per-header grades
  std::vector<std::string> recommendations;
};

/// Grade points used for the weighted overall score.
static int gradePoints(Grade g) {
  switch (g) {
    case Grade::A: return 100;
    case Grade::B: return 70;
    case Grade::C: return 40;
    case Grade::F: return 0;
  }
  return 0;
}

/// One year in seconds - the minimum HSTS max-age worth calling strong.
constexpr long long ONE_YEAR = 31536000;

// --- string helpers (the TS graders are regex-shaped; plain scans are the
//     idiomatic C++ equivalent and carry the same semantics) ------------------

static std::string toLower(std::string s) {
  std::transform(s.begin(), s.end(), s.begin(),
                 [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
  return s;
}

static std::string toUpper(std::string s) {
  std::transform(s.begin(), s.end(), s.begin(),
                 [](unsigned char c) { return static_cast<char>(std::toupper(c)); });
  return s;
}

static std::string trim(const std::string& s) {
  size_t b = s.find_first_not_of(" \t\r\n");
  if (b == std::string::npos) return "";
  size_t e = s.find_last_not_of(" \t\r\n");
  return s.substr(b, e - b + 1);
}

/** Match the TS `;\s*<word>` regexes (HSTS includeSubDomains / preload). */
static bool semicolonDirective(const std::string& value, const std::string& word) {
  const std::string lower = toLower(value);
  const std::string target = toLower(word);
  size_t pos = 0;
  while ((pos = lower.find(';', pos)) != std::string::npos) {
    size_t next = pos + 1;
    while (next < lower.size() && std::isspace(static_cast<unsigned char>(lower[next]))) next++;
    if (startsWith(lower.substr(next), target)) return true;
    pos = next;
  }
  return false;
}

static bool contains(const std::string& haystack, const std::string& needle) {
  return haystack.find(needle) != std::string::npos;
}

static bool containsIgnoreCase(const std::string& haystack, const std::string& needle) {
  return contains(toLower(haystack), toLower(needle));
}

static bool startsWith(const std::string& s, const std::string& prefix) {
  return s.size() >= prefix.size() && s.compare(0, prefix.size(), prefix) == 0;
}

static bool startsWithIgnoreCase(const std::string& s, const std::string& prefix) {
  return s.size() >= prefix.size() && toLower(s.substr(0, prefix.size())) == toLower(prefix);
}

/// max-age\s*=\s*(\d+) — the parsed number, or −1 when absent.
static long long parseMaxAge(const std::string& value) {
  const std::string lower = toLower(value);
  size_t pos = lower.find("max-age");
  if (pos == std::string::npos) return -1;
  pos = lower.find('=', pos);
  if (pos == std::string::npos) return -1;
  pos++;
  size_t digits = pos;
  while (digits < lower.size() && std::isdigit(static_cast<unsigned char>(lower[digits]))) digits++;
  if (digits == pos) return -1;
  return std::stoll(lower.substr(pos, digits - pos));
}

/// (^|\s)default-src\s — a default-src directive at a term boundary.
static bool hasDefaultSrc(const std::string& value) {
  const std::string lower = toLower(value);
  size_t pos = 0;
  while ((pos = lower.find("default-src", pos)) != std::string::npos) {
    const bool leftOk = pos == 0 || std::isspace(static_cast<unsigned char>(lower[pos - 1])) != 0;
    const size_t after = pos + std::string("default-src").size();
    const bool rightOk = after < lower.size() && std::isspace(static_cast<unsigned char>(lower[after])) != 0;
    if (leftOk && rightOk) return true;
    pos = after;
  }
  return false;
}

/**
 * Parse a raw header block into a lowercased-name map. Lines are `name: value`;
 * the `HTTP/1.1 200 OK` status line and any line without a colon are ignored.
 * A line starting with space/tab continues the previous header (RFC 7230
 * obs-fold); a repeated name joins its values with ", ".
 */
std::map<std::string, std::string> parseHeaders(const std::string& raw) {
  std::map<std::string, std::string> headers;
  std::string lastName;
  bool haveLast = false;

  std::vector<std::string> lines;
  std::string cur;
  for (char c : raw) {
    if (c == '\n') {
      if (!cur.empty() && cur.back() == '\r') cur.pop_back();
      lines.push_back(cur);
      cur.clear();
    } else {
      cur += c;
    }
  }
  lines.push_back(cur);

  for (const std::string& line : lines) {
    if (!line.empty() && (line[0] == ' ' || line[0] == '\t')) {
      // Folded continuation - append to the previous header, if any.
      if (haveLast) {
        std::string prev = headers.count(lastName) ? headers[lastName] : "";
        std::string joined = trim(prev + " " + trim(line));
        headers[lastName] = joined;
      }
      continue;
    }
    const size_t colon = line.find(':');
    if (colon == std::string::npos) continue; // status line, blank line, or junk
    const std::string name = trim(line.substr(0, colon));
    if (name.empty()) continue;
    const std::string lowerName = toLower(name);
    const std::string value = trim(line.substr(colon + 1));
    auto it = headers.find(lowerName);
    headers[lowerName] = it == headers.end() ? value : it->second + ", " + value;
    lastName = lowerName;
    haveLast = true;
  }
  return headers;
}

// --- Per-header graders -------------------------------------------------------

struct Grading {
  Grade grade;
  std::string explanation;
  std::string recommendation;
};

static Grading gradeHsts(bool present, const std::string& rawValue) {
  if (!present) {
    return {Grade::C,
            "Missing - the browser accepts plain HTTP, so the first visit (and SSL-strip attacks) can downgrade the connection.",
            "Add: strict-transport-security: max-age=31536000; includeSubDomains; preload"};
  }
  const long long maxAge = parseMaxAge(rawValue);
  if (maxAge < 0) {
    return {Grade::B,
            "Present but malformed - no readable max-age directive (" + rawValue + ").",
            "Use: strict-transport-security: max-age=31536000; includeSubDomains"};
  }
  if (maxAge == 0) {
    return {Grade::F,
            "max-age=0 actively deletes the HSTS policy - the site opts out of HTTPS-only enforcement.",
            "Raise max-age to at least 31536000 (one year)."};
  }
  const bool hasSubs = semicolonDirective(rawValue, "includeSubDomains");
  if (maxAge < ONE_YEAR) {
    return {Grade::B,
            "Present with max-age=" + std::to_string(maxAge) + "s (< 1 year)" +
                (hasSubs ? " and includeSubDomains" : "") +
                " - a shorter window weakens the guarantee.",
            "Raise max-age to 31536000 (one year) or more."};
  }
  if (!hasSubs) {
    return {Grade::B,
            "Strong max-age (>= 1 year), but includeSubDomains is missing - subdomains stay downgradable.",
            "Append ; includeSubDomains once every subdomain serves HTTPS."};
  }
  const bool hasPreload = semicolonDirective(rawValue, "preload");
  return {Grade::A,
          "Strong: max-age >= 1 year + includeSubDomains" +
              (hasPreload ? " + preload - eligible for the browser preload list."
                          : ". Add preload to join the browser preload list."),
          "Best practice met - nothing to change."};
}

static Grading gradeCsp(bool present, const std::string& value) {
  if (!present) {
    return {Grade::C,
            "Missing - no restriction on where scripts and resources load from, so injected markup runs at full power.",
            "Add a policy, e.g.: content-security-policy: default-src 'none'; script-src 'self'; ..."};
  }
  const bool inline_ = containsIgnoreCase(value, "'unsafe-inline'");
  const bool evaluates = containsIgnoreCase(value, "'unsafe-eval'");
  if (inline_ && evaluates) {
    return {Grade::F,
            "Actively harmful - 'unsafe-inline' + 'unsafe-eval' together re-allow inline injection and string evaluation, gutting the policy while it still looks present.",
            "Remove 'unsafe-inline' and 'unsafe-eval'; use nonces or hashes for inline scripts."};
  }
  if (inline_ || evaluates) {
    const std::string culprit = inline_ ? "'unsafe-inline'" : "'unsafe-eval'";
    return {Grade::B,
            "Present but weak - " + culprit + " punches a hole through the anti-XSS guarantee.",
            "Remove " + culprit + "; use nonces or hashes for legitimate inline code."};
  }
  if (!hasDefaultSrc(value)) {
    return {Grade::B,
            "Present with no default-src - resource types not listed explicitly fall back to the browser permissive default.",
            "Start the policy with default-src 'none' or 'self', then allow what you need."};
  }
  return {Grade::A,
          "Strong - a default-src fallback with no unsafe-inline / unsafe-eval escape hatches.",
          "Best practice met - nothing to change."};
}

static Grading gradeFrameOptions(bool present, const std::string& value) {
  if (!present) {
    return {Grade::C,
            "Missing - any site can embed this page in an iframe and overlay it (clickjacking).",
            "Add: x-frame-options: DENY (or SAMEORIGIN if you embed yourself)"};
  }
  const std::string v = trim(value);
  const std::string upper = toUpper(v);
  if (upper == "DENY" || upper == "SAMEORIGIN") {
    return {Grade::A,
            "Strong - " + upper + " blocks framing by other origins.",
            "Best practice met - nothing to change."};
  }
  if (startsWith(upper, "ALLOW-FROM")) {
    return {Grade::B,
            "ALLOW-FROM is deprecated and modern browsers ignore it - the page is effectively frameable.",
            "Replace with a CSP frame-ancestors directive (x-frame-options: DENY as fallback)."};
  }
  return {Grade::B,
          "Unrecognized value (" + value + ") - browsers ignore it, leaving the page frameable.",
          "Use DENY or SAMEORIGIN."};
}

static Grading gradeContentTypeOptions(bool present, const std::string& value) {
  if (!present) {
    return {Grade::C,
            "Missing - browsers may sniff uploaded content into executable types.",
            "Add: x-content-type-options: nosniff"};
  }
  if (toLower(trim(value)) == "nosniff") {
    return {Grade::A,
            "Strong - MIME sniffing is off; the declared Content-Type is final.",
            "Best practice met - nothing to change."};
  }
  return {Grade::B,
          "Present but the value (" + value + ") is not nosniff, so browsers ignore the header.",
          "Send exactly: x-content-type-options: nosniff"};
}

static bool inList(const std::string& v, std::initializer_list<const char*> options) {
  for (const char* option : options) {
    if (v == option) return true;
  }
  return false;
}

static Grading gradeReferrerPolicy(bool present, const std::string& value) {
  if (!present) {
    return {Grade::C,
            "Missing - older browsers default to leaking the full URL to whatever site the user navigates to.",
            "Add: referrer-policy: strict-origin-when-cross-origin (or no-referrer)"};
  }
  const std::string v = toLower(trim(value));
  if (v == "unsafe-url") {
    return {Grade::F,
            "Actively harmful - unsafe-url sends the full URL (including IDs and tokens in query strings) to every destination, even on plain HTTP.",
            "Use strict-origin-when-cross-origin or no-referrer instead."};
  }
  if (inList(v, {"no-referrer", "same-origin", "strict-origin", "strict-origin-when-cross-origin"})) {
    return {Grade::A,
            "Strong - " + v + " leaks at most the origin cross-origin.",
            "Best practice met - nothing to change."};
  }
  if (inList(v, {"origin", "origin-when-cross-origin", "no-referrer-when-downgrade"})) {
    return {Grade::B,
            "Present but leaky - " + v + " still shares the full URL in some same- or cross-origin cases.",
            "Tighten to strict-origin-when-cross-origin, strict-origin, same-origin, or no-referrer."};
  }
  return {Grade::B,
          "Unrecognized value (" + value + ") - browsers fall back to their default policy.",
          "Use one of the standard directives, e.g. strict-origin-when-cross-origin."};
}

static Grading gradePermissionsPolicy(bool present, const std::string& value) {
  if (!present) {
    return {Grade::C,
            "Missing - powerful features (camera, geolocation, payment...) default to the browser policy, not yours.",
            "Add: permissions-policy: camera=(), microphone=(), geolocation=()"};
  }
  std::string v;
  for (char c : toLower(value)) {
    if (!std::isspace(static_cast<unsigned char>(c))) v += c;
  }
  if (v.empty()) {
    return {Grade::B,
            "Present but empty - no feature is restricted, so it declares nothing.",
            "List the features to switch off, e.g. camera=(), microphone=()."};
  }
  if (v == "*" || contains(v, "=*")) {
    return {Grade::B,
            "Present but permissive - a =* wildcard re-allows the listed feature(s) everywhere.",
            "Use an empty allowlist () or a specific origin instead of *."};
  }
  return {Grade::A,
          "Strong - the policy restricts at least one powerful feature.",
          "Best practice met - nothing to change."};
}

static Grading gradeXssProtection(bool present, const std::string& value) {
  if (!present) {
    return {Grade::A,
            "Correctly omitted - the header is deprecated; modern browsers removed the XSS Auditor it drove.",
            "Nothing to change - rely on Content-Security-Policy instead."};
  }
  const std::string v = trim(value);
  if (v == "0" || startsWith(v, "0;")) {
    return {Grade::A,
            "Explicitly disabled ('0') - the right call for a deprecated header old browsers still honor.",
            "Nothing to change - or remove the header entirely."};
  }
  if (startsWith(v, "1")) {
    return {Grade::B,
            "Deprecated - '1; mode=block' is ignored by modern browsers and the old Auditor had cross-site leaks of its own.",
            "Send '0' or drop the header; use Content-Security-Policy for XSS defense."};
  }
  return {Grade::B,
          "Present with an unrecognized value (" + value + ") - remove the deprecated header.",
          "Send '0' or drop the header entirely."};
}

static Grading gradeCoop(bool present, const std::string& value) {
  if (!present) {
    return {Grade::C,
            "Missing - pages that open or are opened by other origins share a browsing context with them.",
            "Add: cross-origin-opener-policy: same-origin"};
  }
  const std::string v = toLower(trim(value));
  if (v == "same-origin") {
    return {Grade::A,
            "Strong - the window handle is isolated from cross-origin openers.",
            "Best practice met - nothing to change."};
  }
  if (v == "same-origin-allow-popups") {
    return {Grade::B,
            "Partially isolated - popups you open keep a reference to this window.",
            "Use same-origin unless you genuinely need cross-origin popup handles."};
  }
  if (v == "unsafe-none") {
    return {Grade::B,
            "Explicit opt-out - unsafe-none restores the shared browsing context COOP exists to cut.",
            "Use same-origin unless a documented integration requires shared handles."};
  }
  return {Grade::B,
          "Unrecognized value (" + value + ") - browsers treat it as no COOP.",
          "Use same-origin."};
}

using Grader = Grading (*)(bool present, const std::string& value);

/// Compile-time-complete grader table: every catalog name has a grader.
static Grader graderFor(const std::string& name) {
  if (name == "Strict-Transport-Security") return gradeHsts;
  if (name == "Content-Security-Policy") return gradeCsp;
  if (name == "X-Frame-Options") return gradeFrameOptions;
  if (name == "X-Content-Type-Options") return gradeContentTypeOptions;
  if (name == "Referrer-Policy") return gradeReferrerPolicy;
  if (name == "Permissions-Policy") return gradePermissionsPolicy;
  if (name == "X-XSS-Protection") return gradeXssProtection;
  return gradeCoop; // Cross-Origin-Opener-Policy
}

/** Map a weighted 0-100 score to the overall A/B/C/F grade. */
static Grade overallGrade(int score) {
  if (score >= 90) return Grade::A;
  if (score >= 70) return Grade::B;
  if (score >= 45) return Grade::C;
  return Grade::F;
}

/**
 * Analyze a raw HTTP response-header block. Every catalog header gets a
 * ParsedHeader row (in catalog order); the overall score is the weight-adjusted
 * average of per-header grade points, and recommendations list one fix per
 * header that is not at grade A.
 */
HeaderAnalysis analyzeHeaders(const std::string& raw) {
  const std::map<std::string, std::string> parsed = parseHeaders(raw);
  const std::vector<HeaderSpec>& specs = securityHeaders();

  double totalWeight = 0;
  double weighted = 0;
  HeaderAnalysis analysis;
  for (const HeaderSpec& spec : specs) {
    const std::string lower = toLower(spec.name);
    const auto it = parsed.find(lower);
    const bool present = it != parsed.end();
    const std::string value = present ? it->second : std::string();
    const Grading g = graderFor(spec.name)(present, value);
    ParsedHeader row;
    row.name = spec.name;
    row.value = value;
    row.present = present;
    row.grade = g.grade;
    row.explanation = g.explanation;
    row.recommendation = g.recommendation;
    analysis.headers.push_back(row);
    totalWeight += spec.weight;
    weighted += static_cast<double>(gradePoints(g.grade)) * spec.weight;
    if (g.grade != Grade::A) analysis.recommendations.push_back(g.recommendation);
  }
  analysis.score = static_cast<int>(std::lround(weighted / totalWeight));
  analysis.grade = overallGrade(analysis.score);
  return analysis;
}

} // namespace sec_headers

Also available in 8 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →