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 →