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 →