CSP Builder — Java 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 Java implementation — the same logic the interactive tool runs, in a shareable, citable form.
// CSP Builder — pure Content-Security-Policy logic, deterministic, no throwing.
//
// Language: Java (17+, standard library only)
// Ported from src/lib/csp-builder.ts
// display source — part of CosmoDev's polyglot tool pages.
//
// A CSP is modeled as an insertion-ordered map of directive -> source list.
// buildCSP assembles the map into the header string (directives in catalog
// order, then any unknown directives in insertion order); parseCSP reads a
// header back into the map. Neither ever throws - parse is lenient by design
// so a pasted real-world header always yields something editable.
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public final class CspBuilder {
/** How a directive takes its value: a source list, a single URL, or a bare flag. */
public enum DirectiveKind { SOURCES, URL, FLAG }
/** How much exposure the directive controls (drives UI emphasis). */
public enum DirectiveRisk { LOW, MEDIUM, HIGH }
/** One entry of the built-in directive catalog. */
public record DirectiveInfo(String name, DirectiveKind kind, String description,
DirectiveRisk risk, List<String> defaultSources) {
}
/** A policy: directive name (lowercase) -> enabled source list. Present key = enabled. */
public static Map<String, List<String>> newPolicy() {
return new LinkedHashMap<>();
}
/** The catalog, in canonical build/display order. */
public static final List<DirectiveInfo> CSP_DIRECTIVES = List.of(
new DirectiveInfo("default-src", DirectiveKind.SOURCES,
"Fallback for every fetch directive you do not set explicitly. Set this first, then tighten individual directives.",
DirectiveRisk.MEDIUM, List.of("'self'")),
new DirectiveInfo("script-src", DirectiveKind.SOURCES,
"Where scripts may load from. The single most important XSS control - keep it as tight as you can.",
DirectiveRisk.HIGH, List.of("'self'")),
new DirectiveInfo("style-src", DirectiveKind.SOURCES,
"Where stylesheets may load from. Also gates inline style attributes.",
DirectiveRisk.MEDIUM, List.of("'self'")),
new DirectiveInfo("img-src", DirectiveKind.SOURCES,
"Where images and favicons may load from.",
DirectiveRisk.LOW, List.of("'self'")),
new DirectiveInfo("connect-src", DirectiveKind.SOURCES,
"Which URLs scripts may connect to (fetch, XHR, WebSocket). Your data-exfiltration boundary.",
DirectiveRisk.MEDIUM, List.of("'self'")),
new DirectiveInfo("font-src", DirectiveKind.SOURCES,
"Where web fonts may load from.",
DirectiveRisk.LOW, List.of("'self'")),
new DirectiveInfo("frame-src", DirectiveKind.SOURCES,
"Which URLs may be embedded as child browsing contexts (iframe, frame).",
DirectiveRisk.LOW, List.of("'self'")),
new DirectiveInfo("media-src", DirectiveKind.SOURCES,
"Where audio and video may load from.",
DirectiveRisk.LOW, List.of("'self'")),
new DirectiveInfo("object-src", DirectiveKind.SOURCES,
"Where plugin content (object, embed, applet) may load from. Almost always should be 'none'.",
DirectiveRisk.HIGH, List.of("'none'")),
new DirectiveInfo("base-uri", DirectiveKind.SOURCES,
"Which URLs may set the document base. Restrict to 'self' to block <base> hijacking of relative URLs.",
DirectiveRisk.HIGH, List.of("'self'")),
new DirectiveInfo("form-action", DirectiveKind.SOURCES,
"Where forms may submit to. Does not fall back to default-src.",
DirectiveRisk.MEDIUM, List.of("'self'")),
new DirectiveInfo("frame-ancestors", DirectiveKind.SOURCES,
"Which parents may embed this page (clickjacking control). Ignored inside a <meta> tag - header delivery only.",
DirectiveRisk.MEDIUM, List.of("'self'")),
new DirectiveInfo("report-uri", DirectiveKind.URL,
"URL where the browser posts violation reports. Pair with a report collector.",
DirectiveRisk.LOW, List.of()),
new DirectiveInfo("upgrade-insecure-requests", DirectiveKind.FLAG,
"Tells the browser to rewrite http:// subresource requests to https://.",
DirectiveRisk.LOW, List.of()),
new DirectiveInfo("block-all-mixed-content", DirectiveKind.FLAG,
"Blocks loading of any http:// subresource on an https:// page.",
DirectiveRisk.LOW, List.of())
);
/** Source presets offered in the UI when adding a source to a directive. */
public static final List<String> COMMON_SOURCES = List.of(
"'self'", "'none'", "'unsafe-inline'", "'unsafe-eval'",
"'strict-dynamic'", "data:", "blob:", "https:");
/** Directives that take no value - emitted as a bare name. */
private static final Set<String> FLAG_DIRECTIVES =
Set.of(CSP_DIRECTIVES.stream().filter(d -> d.kind() == DirectiveKind.FLAG)
.map(DirectiveInfo::name).toArray(String[]::new));
/** Catalog names, for ordering during build. */
private static final Set<String> KNOWN_DIRECTIVES =
Set.of(CSP_DIRECTIVES.stream().map(DirectiveInfo::name).toArray(String[]::new));
/**
* Assemble a policy map into the {@code 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.
*/
public static String buildCSP(Map<String, List<String>> directives) {
List<String> parts = new ArrayList<>();
for (DirectiveInfo d : CSP_DIRECTIVES) emit(directives, parts, d.name());
for (String name : directives.keySet()) {
if (!KNOWN_DIRECTIVES.contains(name)) emit(directives, parts, name);
}
return String.join("; ", parts);
}
private static void emit(Map<String, List<String>> directives, List<String> parts, String name) {
List<String> sources = directives.get(name);
if (sources == null) return;
if (FLAG_DIRECTIVES.contains(name)) {
parts.add(name);
return;
}
if (sources.isEmpty()) return;
parts.add(name + " " + String.join(" ", sources));
}
/**
* 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 {@code 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, empty map out.
*/
public static Map<String, List<String>> parseCSP(String header) {
String text = header == null ? "" : header.trim();
if (text.toLowerCase().matches("content-security-policy\\s*:.*")) {
text = text.substring(text.indexOf(':') + 1);
}
Map<String, List<String>> out = newPolicy();
for (String token : text.split(";")) {
String[] words = token.trim().split("\\s+");
List<String> kept = new ArrayList<>();
for (String w : words) if (!w.isEmpty()) kept.add(w);
if (kept.isEmpty()) continue;
String name = kept.get(0).toLowerCase();
if (out.containsKey(name)) continue;
out.put(name, new ArrayList<>(kept.subList(1, kept.size())));
}
return out;
}
/** Sources treated as security-weakening, compared case-insensitively. */
private static final Set<String> RISKY_SOURCES =
Set.of("'unsafe-inline'", "'unsafe-eval'", "data:", "http:", "*");
/**
* 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.
*/
public static boolean isRiskySource(String source) {
String s = source.trim().toLowerCase();
return RISKY_SOURCES.contains(s) || s.startsWith("http://");
}
/** Short human explanation for each risky source (tooltip text in the UI). */
private static final Map<String, String> RISK_EXPLANATIONS = Map.of(
"'unsafe-inline'",
"Allows inline <script>/<style> and event handlers - defeats most of CSP's XSS protection.",
"'unsafe-eval'",
"Allows eval() and similar code execution - weakens XSS protection.",
"*", "Allows every origin - effectively no restriction for this directive.",
"data:",
"data: URIs can carry arbitrary payloads and are same-origin - attackers can smuggle content through them.",
"http:",
"Allows insecure origins - a network attacker can inject or tamper with subresources.");
/** Explanation for any risky source; falls back to the generic insecure-origin text. */
public static String riskExplanation(String source) {
String key = source.trim().toLowerCase();
return RISK_EXPLANATIONS.getOrDefault(key,
"Insecure http:// URL - traffic can be tampered with in transit.");
}
/**
* One policy problem: either policy-wide (directive = "", source = null)
* or a risky source.
*/
public record CspIssue(String directive, String source, 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.
*/
public static List<CspIssue> validateCSP(Map<String, List<String>> directives) {
List<CspIssue> issues = new ArrayList<>();
if (!directives.containsKey("default-src")) {
issues.add(new CspIssue("", null,
"No default-src - every directive you don't set explicitly falls back to the browser's permissive default."));
}
for (Map.Entry<String, List<String>> entry : directives.entrySet()) {
for (String src : entry.getValue()) {
if (isRiskySource(src)) {
issues.add(new CspIssue(entry.getKey(), src,
entry.getKey() + ": " + src + " weakens this policy - " + riskExplanation(src)));
}
}
}
return issues;
}
/** Score penalty per risky source (case-insensitive key). */
private static final Map<String, Integer> SCORE_PENALTIES = Map.of(
"'unsafe-inline'", 20, "'unsafe-eval'", 15, "*", 20, "data:", 10, "http:", 10);
/**
* Security score, 0-100. Starts at 100; each risky source subtracts its
* penalty (insecure http:// URLs subtract 10), and a missing default-src
* subtracts 10. Clamped to 0-100. Deterministic.
*/
public static int securityScore(Map<String, List<String>> directives) {
int score = 100;
if (!directives.containsKey("default-src")) score -= 10;
for (List<String> sources : directives.values()) {
for (String src : sources) {
String s = src.trim().toLowerCase();
score -= SCORE_PENALTIES.getOrDefault(s, s.startsWith("http://") ? 10 : 0);
}
}
return Math.max(0, Math.min(100, score));
}
private CspBuilder() {
}
}
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 →