Skip to content

CSP Builder — C++ source

Build a Content-Security-Policy header interactively. Toggle directives, add sources, see the assembled header in real time — with a security score that flags unsafe sources.

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

// csp-builder — Content-Security-Policy assembly, parsing, lint and scoring.
//
// Language: C++ (C++17, standard library only)
// Ported from src/lib/csp-builder.ts (the canonical TypeScript implementation).
// display source — part of CosmoDev's polyglot tool pages.
//
// A CSP is modeled as a list of (directive -> source list) entries. buildCSP
// assembles the entries into the header string (directives in catalog order,
// then any unknown directives in insertion order); parseCSP reads a header
// back into the list. Neither function ever throws - parse is lenient by
// design so a pasted real-world header always yields something editable.

#include <algorithm>
#include <cctype>
#include <string>
#include <utility>
#include <vector>

namespace csp_builder {

/** How a directive takes its value: a source list, a single URL, or a bare flag. */
enum class DirectiveKind { Sources, Url, Flag };

/** How much exposure the directive controls (drives UI emphasis). */
enum class DirectiveRisk { Low, Medium, High };

/** One entry of the built-in directive catalog. */
struct DirectiveInfo {
  const char* name;
  DirectiveKind kind;
  const char* description;
  DirectiveRisk risk;
  std::vector<std::string> defaultSources;
};

/**
 * A policy: ordered (directive name, enabled source list) entries, lowercase
 * names. A present entry = enabled. Order is preserved because unknown
 * directives emit in insertion order.
 */
using CSPDirectiveMap = std::vector<std::pair<std::string, std::vector<std::string>>>;

/** The catalog, in canonical build/display order. */
const std::vector<DirectiveInfo>& directiveCatalog() {
  static const std::vector<DirectiveInfo> CATALOG = {
    {"default-src", DirectiveKind::Sources,
     "Fallback for every fetch directive you do not set explicitly. Set this first, then tighten individual directives.",
     DirectiveRisk::Medium, {"'self'"}},
    {"script-src", DirectiveKind::Sources,
     "Where scripts may load from. The single most important XSS control - keep it as tight as you can.",
     DirectiveRisk::High, {"'self'"}},
    {"style-src", DirectiveKind::Sources,
     "Where stylesheets may load from. Also gates inline style attributes.",
     DirectiveRisk::Medium, {"'self'"}},
    {"img-src", DirectiveKind::Sources,
     "Where images and favicons may load from.",
     DirectiveRisk::Low, {"'self'"}},
    {"connect-src", DirectiveKind::Sources,
     "Which URLs scripts may connect to (fetch, XHR, WebSocket). Your data-exfiltration boundary.",
     DirectiveRisk::Medium, {"'self'"}},
    {"font-src", DirectiveKind::Sources,
     "Where web fonts may load from.",
     DirectiveRisk::Low, {"'self'"}},
    {"frame-src", DirectiveKind::Sources,
     "Which URLs may be embedded as child browsing contexts (iframe, frame).",
     DirectiveRisk::Low, {"'self'"}},
    {"media-src", DirectiveKind::Sources,
     "Where audio and video may load from.",
     DirectiveRisk::Low, {"'self'"}},
    {"object-src", DirectiveKind::Sources,
     "Where plugin content (object, embed, applet) may load from. Almost always should be 'none'.",
     DirectiveRisk::High, {"'none'"}},
    {"base-uri", DirectiveKind::Sources,
     "Which URLs may set the document base. Restrict to 'self' to block <base> hijacking of relative URLs.",
     DirectiveRisk::High, {"'self'"}},
    {"form-action", DirectiveKind::Sources,
     "Where forms may submit to. Does not fall back to default-src.",
     DirectiveRisk::Medium, {"'self'"}},
    {"frame-ancestors", DirectiveKind::Sources,
     "Which parents may embed this page (clickjacking control). Ignored inside a <meta> tag - header delivery only.",
     DirectiveRisk::Medium, {"'self'"}},
    {"report-uri", DirectiveKind::Url,
     "URL where the browser posts violation reports. Pair with a report collector.",
     DirectiveRisk::Low, {}},
    {"upgrade-insecure-requests", DirectiveKind::Flag,
     "Tells the browser to rewrite http:// subresource requests to https://.",
     DirectiveRisk::Low, {}},
    {"block-all-mixed-content", DirectiveKind::Flag,
     "Blocks loading of any http:// subresource on an https:// page.",
     DirectiveRisk::Low, {}},
  };
  return CATALOG;
}

/** Source presets offered in the UI when adding a source to a directive. */
const std::vector<std::string>& commonSources() {
  static const std::vector<std::string> SOURCES = {
    "'self'", "'none'", "'unsafe-inline'", "'unsafe-eval'", "'strict-dynamic'",
    "data:", "blob:", "https:",
  };
  return SOURCES;
}

// --- internal lookups ---------------------------------------------------------

static bool isFlagDirective(const std::string& name) {
  for (const auto& d : directiveCatalog()) {
    if (name == d.name) return d.kind == DirectiveKind::Flag;
  }
  return false;
}

static bool isKnownDirective(const std::string& name) {
  for (const auto& d : directiveCatalog()) {
    if (name == d.name) return true;
  }
  return false;
}

static const std::vector<std::string>* findSources(const CSPDirectiveMap& directives,
                                                   const std::string& name) {
  for (const auto& entry : directives) {
    if (entry.first == name) return &entry.second;
  }
  return nullptr;
}

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

static std::string trim(std::string s) {
  const char* ws = " \t\r\n\f\v";
  const auto first = s.find_first_not_of(ws);
  if (first == std::string::npos) return "";
  const auto last = s.find_last_not_of(ws);
  return s.substr(first, last - first + 1);
}

/**
 * Assemble a policy map into the `Content-Security-Policy` header value.
 * Known directives emit in catalog order, unknown directives after them in
 * insertion order. Flag directives emit as a bare name; source/url directives
 * with an empty list are omitted (a valueless directive is invalid CSP).
 * An empty map yields an empty string.
 */
std::string buildCSP(const CSPDirectiveMap& directives) {
  std::vector<std::string> parts;
  auto emit = [&](const std::string& name) {
    const std::vector<std::string>* sources = findSources(directives, name);
    if (sources == nullptr) return;
    if (isFlagDirective(name)) {
      parts.push_back(name);
      return;
    }
    if (sources->empty()) return;
    std::string token = name;
    for (const auto& src : *sources) {
      token += ' ';
      token += src;
    }
    parts.push_back(std::move(token));
  };
  for (const auto& d : directiveCatalog()) emit(d.name);
  for (const auto& entry : directives) {
    if (!isKnownDirective(entry.first)) emit(entry.first);
  }
  std::string out;
  for (std::size_t i = 0; i < parts.size(); i++) {
    if (i > 0) out += "; ";
    out += parts[i];
  }
  return out;
}

/**
 * Parse a CSP header value back into a policy map. Lenient: splits on
 * semicolons and whitespace, lowercases directive names, ignores empty tokens,
 * and strips an optional leading `Content-Security-Policy:` label so a pasted
 * full header line works. Duplicate directives keep only the first occurrence
 * (matching how browsers honor them). Never throws; garbage in, {} out.
 */
CSPDirectiveMap parseCSP(const std::string& header) {
  std::string text = trim(header);
  // Strip a leading "Content-Security-Policy:" label (case-insensitive).
  static const std::string LABEL = "content-security-policy";
  if (text.size() >= LABEL.size() && lower(text.substr(0, LABEL.size())) == LABEL) {
    std::size_t i = LABEL.size();
    while (i < text.size() && text[i] == ' ') i++;
    if (i < text.size() && text[i] == ':') text = text.substr(i + 1);
  }

  CSPDirectiveMap out;
  std::size_t pos = 0;
  while (pos <= text.size()) {
    std::size_t semi = text.find(';', pos);
    std::string token = text.substr(pos, semi == std::string::npos ? std::string::npos
                                                                   : semi - pos);
    // Split the token on runs of whitespace, keeping non-empty words.
    std::vector<std::string> words;
    std::size_t w = 0;
    while (w < token.size()) {
      while (w < token.size() && std::isspace(static_cast<unsigned char>(token[w]))) w++;
      std::size_t start = w;
      while (w < token.size() && !std::isspace(static_cast<unsigned char>(token[w]))) w++;
      if (w > start) words.push_back(token.substr(start, w - start));
    }
    if (!words.empty()) {
      std::string name = lower(words[0]);
      if (findSources(out, name) == nullptr) {
        out.emplace_back(std::move(name),
                         std::vector<std::string>(words.begin() + 1, words.end()));
      }
    }
    if (semi == std::string::npos) break;
    pos = semi + 1;
  }
  return out;
}

/**
 * True when a source weakens the policy: 'unsafe-inline', 'unsafe-eval',
 * 'data:', 'http:', the bare wildcard '*', or any insecure http:// URL.
 * 'self', 'none', 'strict-dynamic', 'blob:', 'https:' and https URLs are fine.
 */
bool isRiskySource(const std::string& source) {
  const std::string s = lower(trim(source));
  return s == "'unsafe-inline'" || s == "'unsafe-eval'" || s == "data:" || s == "http:" ||
         s == "*" || s.rfind("http://", 0) == 0;
}

/** Short human explanation for any risky source; falls back to the generic
 *  insecure-origin text. */
std::string riskExplanation(const std::string& source) {
  const std::string key = lower(trim(source));
  if (key == "'unsafe-inline'")
    return "Allows inline <script>/<style> and event handlers - defeats most of CSP's XSS protection.";
  if (key == "'unsafe-eval'") return "Allows eval() and similar code execution - weakens XSS protection.";
  if (key == "*") return "Allows every origin - effectively no restriction for this directive.";
  if (key == "data:")
    return "data: URIs can carry arbitrary payloads and are same-origin - attackers can smuggle content through them.";
  if (key == "http:") return "Allows insecure origins - a network attacker can inject or tamper with subresources.";
  return "Insecure http:// URL - traffic can be tampered with in transit.";
}

/** One policy problem: either policy-wide (directive empty) or a risky source. */
struct CspIssue {
  /** Directive the issue belongs to; "" for policy-wide issues. */
  std::string directive;
  /** The offending source; empty for policy-wide issues. */
  std::string source;
  std::string message;
};

/**
 * Lint a policy: warns when default-src is missing (unset directives fall back
 * to the browser's allow-everything default) and flags every risky source.
 */
std::vector<CspIssue> validateCSP(const CSPDirectiveMap& directives) {
  std::vector<CspIssue> issues;
  if (findSources(directives, "default-src") == nullptr) {
    issues.push_back({"", "",
      "No default-src - every directive you don't set explicitly falls back to the browser's permissive default."});
  }
  for (const auto& entry : directives) {
    for (const auto& src : entry.second) {
      if (isRiskySource(src)) {
        issues.push_back({entry.first, src,
          entry.first + ": " + src + " weakens this policy - " + riskExplanation(src)});
      }
    }
  }
  return issues;
}

/** Score penalty per risky source (lowercased key; 0 = no penalty). */
static int scorePenalty(const std::string& source) {
  const std::string s = lower(trim(source));
  if (s == "'unsafe-inline'") return 20;
  if (s == "'unsafe-eval'") return 15;
  if (s == "*") return 20;
  if (s == "data:") return 10;
  if (s == "http:") return 10;
  if (s.rfind("http://", 0) == 0) return 10;
  return 0;
}

/**
 * Security score, 0-100. Starts at 100; each risky source subtracts its
 * penalty, and a missing default-src subtracts 10. Clamped to 0-100.
 * Deterministic.
 */
int securityScore(const CSPDirectiveMap& directives) {
  int score = 100;
  if (findSources(directives, "default-src") == nullptr) score -= 10;
  for (const auto& entry : directives) {
    for (const auto& src : entry.second) score -= scorePenalty(src);
  }
  return std::max(0, std::min(100, score));
}

}  // namespace csp_builder

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 →