Skip to content

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 →