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.

/*
 * security-headers — HTTP response security-header analysis and grading.
 *
 * Language: C (C11, standard library only)
 * Source:   CosmoDev polyglot showcase port of the Security Headers tool,
 *           ported from src/lib/security-headers.ts (the canonical TypeScript
 *           implementation).
 * License:  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 fails on content.
 *
 * The TypeScript reference leans on regular expressions; C11 has no regex in
 * its standard library (and POSIX <regex.h> would be overkill for six fixed
 * patterns), so the handful of matches are hand-rolled case-insensitive
 * scanners below. They are exact analogues of the reference's patterns.
 *
 * Build: cc -std=c11 security-headers.c
 */

#include <ctype.h>
#include <math.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* -------------------------------------------------------------------------
 * Public types
 * ------------------------------------------------------------------------- */

/**
 * Per-header grade: A best practice, B present but weak, C missing or
 * dangerous, F actively harmful.
 */
typedef enum {
    GRADE_A,
    GRADE_B,
    GRADE_C,
    GRADE_F
} header_grade;

/** Single-character form, for display. */
char header_grade_char(header_grade grade)
{
    static const char letters[] = { 'A', 'B', 'C', 'F' };
    return letters[grade];
}

/** Catalog entry: canonical name, why the header matters, and its score weight. */
typedef struct {
    const char *name;
    const char *description;
    double weight;
} header_spec;

/** One security header as analyzed: the parsed value plus its grade and guidance. */
typedef struct {
    const char *name;    /* canonical name; points into the catalog */
    char *value;         /* owned raw value; NULL when the header is absent */
    header_grade grade;
    char explanation[640];
    char recommendation[256];
} parsed_header;

#define SECURITY_HEADER_COUNT 8

/** The full analysis result: per-header rows, the overall grade, and fix list. */
typedef struct {
    parsed_header headers[SECURITY_HEADER_COUNT];
    header_grade grade;
    int score; /* 0-100 weighted average of per-header grades */
    const char *recommendations[SECURITY_HEADER_COUNT];
    size_t recommendation_count;
} header_analysis;

/** The 8 security headers this analyzer grades, in display order. */
static const header_spec SECURITY_HEADERS[SECURITY_HEADER_COUNT] = {
    { "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.0 },
    { "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.0 },
    { "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.0 },
    { "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.0 },
    { "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.0 },
    { "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.0 },
};

/** Grade points used for the weighted overall score. */
static int grade_points(header_grade grade)
{
    static const int points[] = { 100, 70, 40, 0 };
    return points[grade];
}

/* One year in seconds - the minimum HSTS max-age worth calling strong. */
#define ONE_YEAR 31536000ULL

/* -------------------------------------------------------------------------
 * Small string helpers (C11 has no strdup, strcasestr, or regex)
 * ------------------------------------------------------------------------- */

static char *sh_strdup(const char *s)
{
    size_t len = strlen(s) + 1;
    char *copy = malloc(len);

    if (copy != NULL) memcpy(copy, s, len);
    return copy;
}

/** Case-insensitive substring search. Returns a pointer into `haystack`, or NULL. */
static const char *sh_stristr(const char *haystack, const char *needle)
{
    size_t n = strlen(needle);

    if (n == 0) return haystack;
    for (const char *p = haystack; *p != '\0'; p++) {
        size_t i = 0;

        while (i < n && p[i] != '\0' &&
               tolower((unsigned char)p[i]) == tolower((unsigned char)needle[i]))
            i++;
        if (i == n) return p;
    }
    return NULL;
}

/** Case-insensitive full-string compare. */
static bool sh_ieq(const char *a, const char *b)
{
    while (*a != '\0' && *b != '\0') {
        if (tolower((unsigned char)*a) != tolower((unsigned char)*b)) return false;
        a++;
        b++;
    }
    return *a == *b;
}

/** In-place trim of leading/trailing whitespace. Returns `s`. */
static char *sh_trim(char *s)
{
    char *start = s;
    size_t len;

    while (*start != '\0' && isspace((unsigned char)*start)) start++;
    len = strlen(start);
    while (len > 0 && isspace((unsigned char)start[len - 1])) len--;
    memmove(s, start, len);
    s[len] = '\0';
    return s;
}

/** Lowercased copy of `s`. Caller frees. */
static char *sh_strdup_lower(const char *s)
{
    char *copy = sh_strdup(s);

    if (copy == NULL) return NULL;
    for (char *p = copy; *p != '\0'; p++)
        *p = (char)tolower((unsigned char)*p);
    return copy;
}

/* -------------------------------------------------------------------------
 * Header map — insertion-ordered, like the reference's Map
 * ------------------------------------------------------------------------- */

typedef struct {
    char *name;  /* owned, lowercase */
    char *value; /* owned */
} header_pair;

typedef struct {
    header_pair *items;
    size_t count;
    size_t capacity;
} header_map;

void header_map_init(header_map *map)
{
    map->items = NULL;
    map->count = 0;
    map->capacity = 0;
}

void header_map_free(header_map *map)
{
    if (map == NULL) return;
    for (size_t i = 0; i < map->count; i++) {
        free(map->items[i].name);
        free(map->items[i].value);
    }
    free(map->items);
    header_map_init(map);
}

/** Value for a lowercase name, or NULL when absent. */
const char *header_map_get(const header_map *map, const char *name)
{
    for (size_t i = 0; i < map->count; i++) {
        if (strcmp(map->items[i].name, name) == 0) return map->items[i].value;
    }
    return NULL;
}

/** Insert or replace. Takes ownership of neither argument. */
static bool header_map_set(header_map *map, const char *name, const char *value)
{
    for (size_t i = 0; i < map->count; i++) {
        if (strcmp(map->items[i].name, name) == 0) {
            char *copy = sh_strdup(value);

            if (copy == NULL) return false;
            free(map->items[i].value);
            map->items[i].value = copy;
            return true;
        }
    }

    if (map->count == map->capacity) {
        size_t cap = (map->capacity == 0) ? 16 : map->capacity * 2;
        header_pair *grown = realloc(map->items, cap * sizeof(*grown));

        if (grown == NULL) return false;
        map->items = grown;
        map->capacity = cap;
    }

    map->items[map->count].name = sh_strdup(name);
    map->items[map->count].value = sh_strdup(value);
    if (map->items[map->count].name == NULL ||
        map->items[map->count].value == NULL) {
        free(map->items[map->count].name);
        free(map->items[map->count].value);
        return false;
    }
    map->count++;
    return true;
}

/**
 * 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 ", ".
 */
bool parse_headers(const char *raw, header_map *map)
{
    const char *cursor;
    char *last_name = NULL;
    bool ok = true;

    header_map_init(map);
    if (raw == NULL) return true;

    cursor = raw;
    while (*cursor != '\0' || cursor == raw) {
        const char *newline = strchr(cursor, '\n');
        const char *line_end = (newline != NULL) ? newline : cursor + strlen(cursor);
        size_t line_len;
        char *line;
        const char *colon;

        /* Trim a trailing \r so \r\n and \n both split cleanly. */
        if (line_end > cursor && line_end[-1] == '\r') line_len = (size_t)(line_end - cursor) - 1;
        else line_len = (size_t)(line_end - cursor);

        line = malloc(line_len + 1);
        if (line == NULL) { ok = false; break; }
        memcpy(line, cursor, line_len);
        line[line_len] = '\0';

        if (line_len > 0 && isspace((unsigned char)line[0])) {
            /* Folded continuation - append to the previous header, if any. */
            if (last_name != NULL) {
                const char *prev = header_map_get(map, last_name);
                size_t prev_len = (prev != NULL) ? strlen(prev) : 0;
                char *folded = malloc(prev_len + line_len + 2);

                if (folded == NULL) { free(line); ok = false; break; }
                snprintf(folded, prev_len + line_len + 2, "%s %s",
                         (prev != NULL) ? prev : "", sh_trim(line));
                sh_trim(folded);
                if (!header_map_set(map, last_name, folded)) {
                    free(folded);
                    free(line);
                    ok = false;
                    break;
                }
                free(folded);
            }
            free(line);
            goto next_line;
        }

        colon = strchr(line, ':');
        if (colon == NULL) { /* status line, blank line, or junk */
            free(line);
            goto next_line;
        }

        {
            size_t name_len = (size_t)(colon - line);
            char *name = malloc(name_len + 1);
            char *value;
            const char *prev;

            if (name == NULL) { free(line); ok = false; break; }
            memcpy(name, line, name_len);
            name[name_len] = '\0';
            sh_trim(name);

            if (name[0] == '\0') {
                free(name);
                free(line);
                goto next_line;
            }
            for (char *p = name; *p != '\0'; p++)
                *p = (char)tolower((unsigned char)*p);

            value = sh_strdup(colon + 1);
            if (value == NULL) { free(name); free(line); ok = false; break; }
            sh_trim(value);

            prev = header_map_get(map, name);
            if (prev != NULL) {
                size_t joined_len = strlen(prev) + strlen(value) + 3;
                char *joined = malloc(joined_len);

                if (joined == NULL) {
                    free(value); free(name); free(line);
                    ok = false;
                    break;
                }
                snprintf(joined, joined_len, "%s, %s", prev, value);
                ok = header_map_set(map, name, joined);
                free(joined);
            } else {
                ok = header_map_set(map, name, value);
            }
            free(value);

            if (!ok) { free(name); free(line); break; }

            free(last_name);
            last_name = name; /* transfers ownership */
            free(line);
        }

    next_line:
        if (newline == NULL) break;
        cursor = newline + 1;
    }

    free(last_name);
    if (!ok) header_map_free(map);
    return ok;
}

/* -------------------------------------------------------------------------
 * Per-header graders: value (NULL when absent) -> grade + guidance
 * ------------------------------------------------------------------------- */

/** The verdict fields every grader returns; folded into parsed_header. */
typedef struct {
    header_grade grade;
    char explanation[640];
    char recommendation[256];
} grading;

static void grading_set(grading *out, header_grade grade,
                        const char *explanation, const char *recommendation)
{
    out->grade = grade;
    snprintf(out->explanation, sizeof(out->explanation), "%s", explanation);
    snprintf(out->recommendation, sizeof(out->recommendation), "%s", recommendation);
}

/**
 * Analogue of /max-age\s*=\s*(\d+)/i — finds the first max-age directive and
 * writes its value to `*max_age`. Saturates instead of overflowing.
 */
static bool match_max_age(const char *value, unsigned long long *max_age)
{
    const char *p = value;

    while ((p = sh_stristr(p, "max-age")) != NULL) {
        const char *q = p + 7;

        while (*q != '\0' && isspace((unsigned char)*q)) q++;
        if (*q != '=') { p += 7; continue; }
        q++;
        while (*q != '\0' && isspace((unsigned char)*q)) q++;
        if (!isdigit((unsigned char)*q)) { p += 7; continue; }

        {
            unsigned long long n = 0;

            while (isdigit((unsigned char)*q)) {
                if (n < ONE_YEAR * 1000ULL) n = n * 10 + (unsigned)(*q - '0');
                q++;
            }
            *max_age = n;
            return true;
        }
    }
    return false;
}

/** Analogue of /;\s*<token>/i — a semicolon-delimited directive. */
static bool match_semicolon_token(const char *value, const char *token)
{
    for (const char *p = strchr(value, ';'); p != NULL; p = strchr(p + 1, ';')) {
        const char *q = p + 1;

        while (*q != '\0' && isspace((unsigned char)*q)) q++;
        if (sh_stristr(q, token) == q) return true;
    }
    return false;
}

/** Analogue of /(^|\s)default-src\s/i. */
static bool match_default_src(const char *value)
{
    for (const char *p = value; *p != '\0'; p++) {
        if (p != value && !isspace((unsigned char)*p)) continue;
        {
            const char *q = (p == value) ? p : p + 1;

            if (sh_stristr(q, "default-src") == q &&
                isspace((unsigned char)q[11]))
                return true;
        }
    }
    return false;
}

static void grade_hsts(const char *value, grading *out)
{
    unsigned long long max_age = 0;
    bool has_subs, has_preload;

    if (value == NULL) {
        grading_set(out, 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");
        return;
    }
    if (!match_max_age(value, &max_age)) {
        out->grade = GRADE_B;
        snprintf(out->explanation, sizeof(out->explanation),
                 "Present but malformed - no readable max-age directive (%s).",
                 value);
        snprintf(out->recommendation, sizeof(out->recommendation),
                 "Use: strict-transport-security: max-age=31536000; "
                 "includeSubDomains");
        return;
    }
    if (max_age == 0) {
        grading_set(out, 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).");
        return;
    }

    has_subs = match_semicolon_token(value, "includesubdomains");
    if (max_age < ONE_YEAR) {
        out->grade = GRADE_B;
        snprintf(out->explanation, sizeof(out->explanation),
                 "Present with max-age=%llus (< 1 year)%s - a shorter window "
                 "weakens the guarantee.",
                 max_age, has_subs ? " and includeSubDomains" : "");
        snprintf(out->recommendation, sizeof(out->recommendation),
                 "Raise max-age to 31536000 (one year) or more.");
        return;
    }
    if (!has_subs) {
        grading_set(out, GRADE_B,
                    "Strong max-age (>= 1 year), but includeSubDomains is "
                    "missing - subdomains stay downgradable.",
                    "Append ; includeSubDomains once every subdomain serves "
                    "HTTPS.");
        return;
    }

    has_preload = match_semicolon_token(value, "preload");
    out->grade = GRADE_A;
    snprintf(out->explanation, sizeof(out->explanation),
             "Strong: max-age >= 1 year + includeSubDomains%s",
             has_preload ? " + preload - eligible for the browser preload list."
                         : ". Add preload to join the browser preload list.");
    snprintf(out->recommendation, sizeof(out->recommendation),
             "Best practice met - nothing to change.");
}

static void grade_csp(const char *value, grading *out)
{
    bool inline_allowed, eval_allowed;

    if (value == NULL) {
        grading_set(out, 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'; ...");
        return;
    }

    inline_allowed = (sh_stristr(value, "'unsafe-inline'") != NULL);
    eval_allowed = (sh_stristr(value, "'unsafe-eval'") != NULL);

    if (inline_allowed && eval_allowed) {
        grading_set(out, 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.");
        return;
    }
    if (inline_allowed || eval_allowed) {
        const char *culprit = inline_allowed ? "'unsafe-inline'" : "'unsafe-eval'";

        out->grade = GRADE_B;
        snprintf(out->explanation, sizeof(out->explanation),
                 "Present but weak - %s punches a hole through the anti-XSS "
                 "guarantee.", culprit);
        snprintf(out->recommendation, sizeof(out->recommendation),
                 "Remove %s; use nonces or hashes for legitimate inline code.",
                 culprit);
        return;
    }
    if (!match_default_src(value)) {
        grading_set(out, 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;
    }
    grading_set(out, GRADE_A,
                "Strong - a default-src fallback with no unsafe-inline / "
                "unsafe-eval escape hatches.",
                "Best practice met - nothing to change.");
}

static void grade_frame_options(const char *value, grading *out)
{
    char *upper;

    if (value == NULL) {
        grading_set(out, 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)");
        return;
    }

    upper = sh_strdup(value);
    if (upper == NULL) { grading_set(out, GRADE_B, "", ""); return; }
    sh_trim(upper);
    for (char *p = upper; *p != '\0'; p++)
        *p = (char)toupper((unsigned char)*p);

    if (strcmp(upper, "DENY") == 0 || strcmp(upper, "SAMEORIGIN") == 0) {
        out->grade = GRADE_A;
        snprintf(out->explanation, sizeof(out->explanation),
                 "Strong - %s blocks framing by other origins.", upper);
        snprintf(out->recommendation, sizeof(out->recommendation),
                 "Best practice met - nothing to change.");
    } else if (strncmp(upper, "ALLOW-FROM", 10) == 0) {
        grading_set(out, 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).");
    } else {
        out->grade = GRADE_B;
        snprintf(out->explanation, sizeof(out->explanation),
                 "Unrecognized value (%s) - browsers ignore it, leaving the "
                 "page frameable.", value);
        snprintf(out->recommendation, sizeof(out->recommendation),
                 "Use DENY or SAMEORIGIN.");
    }
    free(upper);
}

static void grade_content_type_options(const char *value, grading *out)
{
    char *trimmed;

    if (value == NULL) {
        grading_set(out, GRADE_C,
                    "Missing - browsers may sniff uploaded content into "
                    "executable types.",
                    "Add: x-content-type-options: nosniff");
        return;
    }

    trimmed = sh_strdup(value);
    if (trimmed == NULL) { grading_set(out, GRADE_B, "", ""); return; }
    sh_trim(trimmed);

    if (sh_ieq(trimmed, "nosniff")) {
        grading_set(out, GRADE_A,
                    "Strong - MIME sniffing is off; the declared Content-Type "
                    "is final.",
                    "Best practice met - nothing to change.");
    } else {
        out->grade = GRADE_B;
        snprintf(out->explanation, sizeof(out->explanation),
                 "Present but the value (%s) is not nosniff, so browsers "
                 "ignore the header.", value);
        snprintf(out->recommendation, sizeof(out->recommendation),
                 "Send exactly: x-content-type-options: nosniff");
    }
    free(trimmed);
}

static const char *const REFERRER_STRICT[] = {
    "no-referrer", "same-origin", "strict-origin",
    "strict-origin-when-cross-origin", NULL
};
static const char *const REFERRER_WEAK[] = {
    "origin", "origin-when-cross-origin", "no-referrer-when-downgrade", NULL
};

static bool in_list(const char *const *list, const char *needle)
{
    for (size_t i = 0; list[i] != NULL; i++) {
        if (strcmp(list[i], needle) == 0) return true;
    }
    return false;
}

static void grade_referrer_policy(const char *value, grading *out)
{
    char *v;

    if (value == NULL) {
        grading_set(out, 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)");
        return;
    }

    v = sh_strdup_lower(value);
    if (v == NULL) { grading_set(out, GRADE_B, "", ""); return; }
    sh_trim(v);

    if (strcmp(v, "unsafe-url") == 0) {
        grading_set(out, 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.");
    } else if (in_list(REFERRER_STRICT, v)) {
        out->grade = GRADE_A;
        snprintf(out->explanation, sizeof(out->explanation),
                 "Strong - %s leaks at most the origin cross-origin.", v);
        snprintf(out->recommendation, sizeof(out->recommendation),
                 "Best practice met - nothing to change.");
    } else if (in_list(REFERRER_WEAK, v)) {
        out->grade = GRADE_B;
        snprintf(out->explanation, sizeof(out->explanation),
                 "Present but leaky - %s still shares the full URL in some "
                 "same- or cross-origin cases.", v);
        snprintf(out->recommendation, sizeof(out->recommendation),
                 "Tighten to strict-origin-when-cross-origin, strict-origin, "
                 "same-origin, or no-referrer.");
    } else {
        out->grade = GRADE_B;
        snprintf(out->explanation, sizeof(out->explanation),
                 "Unrecognized value (%s) - browsers fall back to their "
                 "default policy.", value);
        snprintf(out->recommendation, sizeof(out->recommendation),
                 "Use one of the standard directives, e.g. "
                 "strict-origin-when-cross-origin.");
    }
    free(v);
}

static void grade_permissions_policy(const char *value, grading *out)
{
    char *v;
    size_t o = 0;

    if (value == NULL) {
        grading_set(out, GRADE_C,
                    "Missing - powerful features (camera, geolocation, "
                    "payment...) default to the browser policy, not yours.",
                    "Add: permissions-policy: camera=(), microphone=(), "
                    "geolocation=()");
        return;
    }

    /* value.replace(/\s+/g, '').toLowerCase() */
    v = malloc(strlen(value) + 1);
    if (v == NULL) { grading_set(out, GRADE_B, "", ""); return; }
    for (const char *p = value; *p != '\0'; p++) {
        if (!isspace((unsigned char)*p))
            v[o++] = (char)tolower((unsigned char)*p);
    }
    v[o] = '\0';

    if (v[0] == '\0') {
        grading_set(out, GRADE_B,
                    "Present but empty - no feature is restricted, so it "
                    "declares nothing.",
                    "List the features to switch off, e.g. camera=(), "
                    "microphone=().");
    } else if (strcmp(v, "*") == 0 || strstr(v, "=*") != NULL) {
        grading_set(out, GRADE_B,
                    "Present but permissive - a =* wildcard re-allows the "
                    "listed feature(s) everywhere.",
                    "Use an empty allowlist () or a specific origin instead "
                    "of *.");
    } else {
        grading_set(out, GRADE_A,
                    "Strong - the policy restricts at least one powerful "
                    "feature.",
                    "Best practice met - nothing to change.");
    }
    free(v);
}

static void grade_xss_protection(const char *value, grading *out)
{
    char *v;

    if (value == NULL) {
        grading_set(out, 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.");
        return;
    }

    v = sh_strdup(value);
    if (v == NULL) { grading_set(out, GRADE_B, "", ""); return; }
    sh_trim(v);

    if (strcmp(v, "0") == 0 || strncmp(v, "0;", 2) == 0) {
        grading_set(out, GRADE_A,
                    "Explicitly disabled ('0') - the right call for a "
                    "deprecated header old browsers still honor.",
                    "Nothing to change - or remove the header entirely.");
    } else if (v[0] == '1') {
        grading_set(out, 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.");
    } else {
        out->grade = GRADE_B;
        snprintf(out->explanation, sizeof(out->explanation),
                 "Present with an unrecognized value (%s) - remove the "
                 "deprecated header.", value);
        snprintf(out->recommendation, sizeof(out->recommendation),
                 "Send '0' or drop the header entirely.");
    }
    free(v);
}

static void grade_coop(const char *value, grading *out)
{
    char *v;

    if (value == NULL) {
        grading_set(out, 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");
        return;
    }

    v = sh_strdup_lower(value);
    if (v == NULL) { grading_set(out, GRADE_B, "", ""); return; }
    sh_trim(v);

    if (strcmp(v, "same-origin") == 0) {
        grading_set(out, GRADE_A,
                    "Strong - the window handle is isolated from cross-origin "
                    "openers.",
                    "Best practice met - nothing to change.");
    } else if (strcmp(v, "same-origin-allow-popups") == 0) {
        grading_set(out, GRADE_B,
                    "Partially isolated - popups you open keep a reference to "
                    "this window.",
                    "Use same-origin unless you genuinely need cross-origin "
                    "popup handles.");
    } else if (strcmp(v, "unsafe-none") == 0) {
        grading_set(out, 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.");
    } else {
        out->grade = GRADE_B;
        snprintf(out->explanation, sizeof(out->explanation),
                 "Unrecognized value (%s) - browsers treat it as no COOP.",
                 value);
        snprintf(out->recommendation, sizeof(out->recommendation),
                 "Use same-origin.");
    }
    free(v);
}

/* Grader table: index-aligned with SECURITY_HEADERS. */
typedef void (*grader_fn)(const char *value, grading *out);

static const grader_fn GRADERS[SECURITY_HEADER_COUNT] = {
    grade_hsts,
    grade_csp,
    grade_frame_options,
    grade_content_type_options,
    grade_referrer_policy,
    grade_permissions_policy,
    grade_xss_protection,
    grade_coop,
};

/* -------------------------------------------------------------------------
 * analyze
 * ------------------------------------------------------------------------- */

/** Map a weighted 0-100 score to the overall A/B/C/F grade. */
static header_grade overall_grade(int score)
{
    if (score >= 90) return GRADE_A;
    if (score >= 70) return GRADE_B;
    if (score >= 45) return GRADE_C;
    return GRADE_F;
}

/** Release the owned values inside an analysis. */
void header_analysis_free(header_analysis *analysis)
{
    if (analysis == NULL) return;
    for (size_t i = 0; i < SECURITY_HEADER_COUNT; i++) {
        free(analysis->headers[i].value);
        analysis->headers[i].value = NULL;
    }
    analysis->recommendation_count = 0;
}

/**
 * Analyze a raw HTTP response-header block. Every catalog header gets a
 * parsed_header 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. Returns false only on allocation
 * failure.
 */
bool analyze_headers(const char *raw, header_analysis *analysis)
{
    header_map parsed;
    double total_weight = 0.0;
    double weighted = 0.0;

    memset(analysis, 0, sizeof(*analysis));

    if (!parse_headers(raw, &parsed)) return false;

    for (size_t i = 0; i < SECURITY_HEADER_COUNT; i++) {
        const header_spec *spec = &SECURITY_HEADERS[i];
        parsed_header *row = &analysis->headers[i];
        char *lower_name = sh_strdup_lower(spec->name);
        const char *value;
        grading verdict;

        if (lower_name == NULL) {
            header_map_free(&parsed);
            header_analysis_free(analysis);
            return false;
        }
        value = header_map_get(&parsed, lower_name);
        free(lower_name);

        GRADERS[i](value, &verdict);

        row->name = spec->name;
        row->value = (value != NULL) ? sh_strdup(value) : NULL;
        if (value != NULL && row->value == NULL) {
            header_map_free(&parsed);
            header_analysis_free(analysis);
            return false;
        }
        row->grade = verdict.grade;
        memcpy(row->explanation, verdict.explanation, sizeof(row->explanation));
        memcpy(row->recommendation, verdict.recommendation,
               sizeof(row->recommendation));

        total_weight += spec->weight;
        weighted += grade_points(verdict.grade) * spec->weight;
    }

    header_map_free(&parsed);

    analysis->score = (int)floor(weighted / total_weight + 0.5);
    analysis->grade = overall_grade(analysis->score);

    for (size_t i = 0; i < SECURITY_HEADER_COUNT; i++) {
        if (analysis->headers[i].grade != GRADE_A) {
            analysis->recommendations[analysis->recommendation_count++] =
                analysis->headers[i].recommendation;
        }
    }
    return true;
}

/* -------------------------------------------------------------------------
 * Demo
 * ------------------------------------------------------------------------- */

int main(void)
{
    static const char *const RAW =
        "HTTP/1.1 200 OK\r\n"
        "Strict-Transport-Security: max-age=31536000; includeSubDomains; preload\r\n"
        "Content-Security-Policy: default-src 'self'; script-src 'self'\r\n"
        "X-Content-Type-Options: nosniff\r\n"
        "Referrer-Policy: strict-origin-when-cross-origin\r\n"
        "X-Frame-Options: DENY\r\n";
    header_analysis analysis;

    if (!analyze_headers(RAW, &analysis)) {
        fprintf(stderr, "analysis failed: out of memory\n");
        return 1;
    }

    printf("Overall: %c (%d/100)\n\n", header_grade_char(analysis.grade),
           analysis.score);
    for (size_t i = 0; i < SECURITY_HEADER_COUNT; i++) {
        const parsed_header *row = &analysis.headers[i];

        printf("[%c] %-28s %s\n", header_grade_char(row->grade), row->name,
               (row->value != NULL) ? row->value : "(absent)");
    }

    if (analysis.recommendation_count > 0) {
        printf("\nRecommendations:\n");
        for (size_t i = 0; i < analysis.recommendation_count; i++)
            printf("  - %s\n", analysis.recommendations[i]);
    }

    header_analysis_free(&analysis);
    return 0;
}

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 →