Skip to content

Permissions-Policy Builder — C source

Build a Permissions-Policy header interactively. Control which browser features (camera, microphone, geolocation, etc.) your site can use.

This is the C implementation — the same logic the interactive tool runs, in a shareable, citable form.

/*
 * permissions-policy — Permissions-Policy header builder, parser, validator and
 *                      privacy scorer.
 *
 * Language: C (C11, standard library only)
 * Source:   CosmoDev polyglot showcase port of the Permissions-Policy Builder
 *           tool, ported from src/lib/permissions-policy.ts (the canonical
 *           TypeScript implementation).
 * License:  display source — part of CosmoDev's polyglot tool pages.
 *
 * The Permissions-Policy header is a comma-separated list of directives:
 *   Permissions-Policy: geolocation=(self), camera=(), microphone=*, usb=(https://a.example)
 * Each directive maps a browser feature to an allowlist. An empty allowlist
 * `()` disables the feature outright; `*` allows it everywhere; `self` limits it
 * to the page's own origin; anything else is a space-separated origin list.
 *
 * A policy is modeled here as a list of feature -> allowlist entries:
 *   camera=()                      -> 0 tokens   (disabled)
 *   microphone=*                   -> ["*"]      (every origin)
 *   geolocation=(self)             -> ["self"]
 *   usb=(https://a.example ...)    -> origin list
 * Features absent from the map are absent from the header (browser default).
 *
 * The TS reference leans on regexes; C has none in its standard library, so the
 * two token grammars are hand-written predicates below — same accepted language,
 * no dependency.
 *
 * Build: cc -std=c11 permissions-policy.c
 */

#include <ctype.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h> /* strncasecmp (POSIX) */

/* --------------------------------------------------------------- catalogue --- */

typedef enum {
    IMPACT_HIGH,
    IMPACT_MEDIUM,
    IMPACT_LOW
} privacy_impact;

typedef struct {
    /* The directive token used in the header, e.g. `geolocation`. */
    const char    *name;
    const char    *description;
    privacy_impact impact;
    /* What browsers do when the feature is absent from the policy. */
    const char    *default_browser_behavior;
} feature_info;

static const feature_info FEATURES[] = {
    /* --- high privacy impact ------------------------------------------------- */
    { "camera", "Access the device camera for photos / video calls.", IMPACT_HIGH, "Same-origin only; prompts the user." },
    { "microphone", "Capture audio from the device microphone.", IMPACT_HIGH, "Same-origin only; prompts the user." },
    { "geolocation", "Read the precise GPS location of the visitor.", IMPACT_HIGH, "Same-origin only; prompts the user." },
    { "display-capture", "Screen / window sharing via getDisplayMedia.", IMPACT_HIGH, "Same-origin only; prompts the user." },
    { "idle-detection", "Detects when the user is away from the device - reveals usage patterns.", IMPACT_HIGH, "Disabled; prompts the user." },
    { "serial", "Talk to serial devices (Arduinos, POS terminals) over a physical port.", IMPACT_HIGH, "Disabled; prompts the user." },
    { "usb", "WebUSB - direct access to connected USB devices.", IMPACT_HIGH, "Disabled; prompts the user." },
    { "hid", "Human Interface Devices - raw access to unusual keyboards, gamepads, sensors.", IMPACT_HIGH, "Disabled; prompts the user." },
    { "xr-spatial-tracking", "Tracks head / hand position in WebXR sessions.", IMPACT_HIGH, "Same-origin only; prompts the user." },
    /* --- medium privacy impact ----------------------------------------------- */
    { "accelerometer", "Device motion sensor - can fingerprint and infer behaviour.", IMPACT_MEDIUM, "Same-origin only." },
    { "ambient-light-sensor", "Reads ambient light level around the device.", IMPACT_MEDIUM, "Same-origin only." },
    { "battery", "Battery Status API - a classic fingerprinting vector.", IMPACT_MEDIUM, "Same-origin only." },
    { "gyroscope", "Device orientation sensor - fingerprinting and behaviour inference.", IMPACT_MEDIUM, "Same-origin only." },
    { "magnetometer", "Compass readings - can leak details of the user's surroundings.", IMPACT_MEDIUM, "Same-origin only." },
    { "keyboard-map", "Reads the physical keyboard layout - a small but real fingerprint.", IMPACT_MEDIUM, "Same-origin only." },
    { "gamepad", "Enumerates connected controllers and their button state.", IMPACT_MEDIUM, "Same-origin only." },
    { "midi", "Web MIDI - access to attached music hardware.", IMPACT_MEDIUM, "Same-origin only; prompts the user." },
    { "payment", "Payment Request API - can expose stored payment handles.", IMPACT_MEDIUM, "Same-origin only." },
    { "publickey-credentials-get", "WebAuthn credential requests.", IMPACT_MEDIUM, "Same-origin only." },
    { "screen-wake-lock", "Keeps the screen awake - drains battery and signals intent.", IMPACT_MEDIUM, "Same-origin only." },
    { "speaker-selection", "Enumerates and switches audio output devices.", IMPACT_MEDIUM, "Same-origin only." },
    { "web-share", "Invokes the OS share sheet with chosen content.", IMPACT_MEDIUM, "Same-origin only." },
    { "encrypted-media", "DRM (EME) - playback identity can be correlated.", IMPACT_MEDIUM, "Same-origin only." },
    { "document-domain", "Let frames relax the same-origin policy via document.domain.", IMPACT_MEDIUM, "Allowed in same-origin pages; deprecated." },
    /* --- low privacy impact -------------------------------------------------- */
    { "autoplay", "Autoplay media with/without sound - not a data leak, an annoyance knob.", IMPACT_LOW, "Muted autoplay allowed; audible blocked." },
    { "cross-origin-isolated", "COOP/COEP isolation for SharedArrayBuffer - hardens the page.", IMPACT_LOW, "Not isolated." },
    { "fullscreen", "Element.requestFullscreen().", IMPACT_LOW, "Same-origin only." },
    { "navigation-override", "Lets a frame intercept its own top-level navigations.", IMPACT_LOW, "Disabled." },
    { "picture-in-picture", "Floating always-on-top video window.", IMPACT_LOW, "Same-origin only." },
};

static const size_t FEATURE_COUNT = sizeof FEATURES / sizeof FEATURES[0];

/* --------------------------------------------------------------- policy map --- */

enum {
    MAX_TOKENS     = 16,  /* allowlist origins per directive */
    MAX_DIRECTIVES = 64,  /* directives per policy */
    MAX_TOKEN_LEN  = 128,
    MAX_NAME_LEN   = 64,
    MAX_ERRORS     = 32,
    MAX_ERROR_LEN  = 200
};

/* One `feature=allowlist` pair. `token_count == 0` means `()`, i.e. disabled. */
typedef struct {
    char name[MAX_NAME_LEN];
    char tokens[MAX_TOKENS][MAX_TOKEN_LEN];
    size_t token_count;
} policy_entry;

/* feature -> allowlist. Entries are kept in insertion order; the builder sorts. */
typedef struct {
    policy_entry entries[MAX_DIRECTIVES];
    size_t count;
} feature_map;

typedef struct {
    bool valid;
    char errors[MAX_ERRORS][MAX_ERROR_LEN];
    size_t error_count;
} validation_result;

/* Append a feature with its allowlist. `tokens` may be NULL when count is 0. */
bool feature_map_set(feature_map *map, const char *name,
                     const char *const *tokens, size_t token_count)
{
    policy_entry *entry;

    if (map == NULL || map->count >= MAX_DIRECTIVES || token_count > MAX_TOKENS) {
        return false;
    }
    entry = &map->entries[map->count++];
    snprintf(entry->name, sizeof entry->name, "%s", name);
    entry->token_count = token_count;
    for (size_t i = 0; i < token_count; i++) {
        snprintf(entry->tokens[i], MAX_TOKEN_LEN, "%s", tokens[i]);
    }
    return true;
}

/* Look up a feature's allowlist, or NULL when it is absent from the policy. */
const policy_entry *feature_map_get(const feature_map *map, const char *name)
{
    if (map == NULL) {
        return NULL;
    }
    for (size_t i = 0; i < map->count; i++) {
        if (strcmp(map->entries[i].name, name) == 0) {
            return &map->entries[i];
        }
    }
    return NULL;
}

/* ------------------------------------------------------------ token grammar --- */

/* FEATURE_NAME_RE: /^[a-z][a-z0-9-]*$/ */
static bool is_feature_name(const char *s)
{
    if (s == NULL || !islower((unsigned char) s[0])) {
        return false;
    }
    for (const char *p = s + 1; *p != '\0'; p++) {
        if (!islower((unsigned char) *p) && !isdigit((unsigned char) *p) && *p != '-') {
            return false;
        }
    }
    return true;
}

/* ORIGIN_RE: /^(https?:\/\/|https?:)[\w.-]+(:\d+)?$/i
 * i.e. an http/https scheme (with or without "//"), a host of word characters,
 * dots and dashes, and an optional numeric port. */
static bool is_origin(const char *s)
{
    const char *p = s;
    size_t host_len = 0;

    if (s == NULL) {
        return false;
    }
    if (strncasecmp(p, "https:", 6) == 0) {
        p += 6;
    } else if (strncasecmp(p, "http:", 5) == 0) {
        p += 5;
    } else {
        return false;
    }
    if (strncmp(p, "//", 2) == 0) {
        p += 2;
    }

    /* Host: one or more of [A-Za-z0-9_.-]. */
    while (*p != '\0' && (isalnum((unsigned char) *p) || *p == '_' || *p == '.' || *p == '-')) {
        p++;
        host_len++;
    }
    if (host_len == 0) {
        return false;
    }

    /* Optional :port, digits only. */
    if (*p == ':') {
        p++;
        if (!isdigit((unsigned char) *p)) {
            return false;
        }
        while (isdigit((unsigned char) *p)) {
            p++;
        }
    }
    return *p == '\0';
}

/* An allowlist token is `self`, `*`, or a valid origin. */
static bool is_allowlist_token(const char *token)
{
    return strcmp(token, "self") == 0 || strcmp(token, "*") == 0 || is_origin(token);
}

/* ------------------------------------------------------------ string helpers --- */

/* Trim ASCII whitespace from both ends, in place. */
static char *trim(char *s)
{
    char *end;

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

/* Strip one layer of surrounding double quotes, as /^"(.*)"$/ does. */
static void unquote(char *s)
{
    size_t len = strlen(s);

    if (len >= 2 && s[0] == '"' && s[len - 1] == '"') {
        memmove(s, s + 1, len - 2);
        s[len - 2] = '\0';
    }
}

/* Drop an optional leading `Permissions-Policy:` (case-insensitive) and trim.
 * Returns a pointer into `buf`, which receives a mutable copy of `header`. */
static char *strip_header_prefix(const char *header, char *buf, size_t buf_size)
{
    char *s;

    snprintf(buf, buf_size, "%s", (header != NULL) ? header : "");
    s = trim(buf);

    if (strncasecmp(s, "permissions-policy", 18) == 0) {
        char *p = s + 18;
        while (*p != '\0' && isspace((unsigned char) *p)) {
            p++;
        }
        if (*p == ':') {
            p++;
            while (*p != '\0' && isspace((unsigned char) *p)) {
                p++;
            }
            s = p;
        }
    }
    return s;
}

/* ------------------------------------------------------------------- build --- */

static int compare_entries(const void *a, const void *b)
{
    return strcmp(((const policy_entry *) a)->name, ((const policy_entry *) b)->name);
}

/*
 * Assemble a Permissions-Policy header value from a feature map. Directives are
 * emitted in sorted order so the same policy always renders the same string.
 * Writes into `out` and returns it.
 */
char *build_permissions_policy(const feature_map *map, char *out, size_t out_size)
{
    policy_entry sorted[MAX_DIRECTIVES];
    size_t written = 0;

    out[0] = '\0';
    if (map == NULL || map->count == 0) {
        return out;
    }

    memcpy(sorted, map->entries, map->count * sizeof sorted[0]);
    qsort(sorted, map->count, sizeof sorted[0], compare_entries);

    for (size_t i = 0; i < map->count && written + 1 < out_size; i++) {
        const policy_entry *e = &sorted[i];
        int n;

        if (i > 0) {
            n = snprintf(out + written, out_size - written, ", ");
            if (n < 0) break;
            written += (size_t) n;
        }

        /* A lone `*` is written bare; everything else is parenthesised, so an
         * empty allowlist renders as `()` — the "disabled" form. */
        if (e->token_count == 1 && strcmp(e->tokens[0], "*") == 0) {
            n = snprintf(out + written, out_size - written, "%s=*", e->name);
            if (n < 0) break;
            written += (size_t) n;
            continue;
        }

        n = snprintf(out + written, out_size - written, "%s=(", e->name);
        if (n < 0) break;
        written += (size_t) n;

        for (size_t t = 0; t < e->token_count && written + 1 < out_size; t++) {
            n = snprintf(out + written, out_size - written, "%s%s",
                         (t > 0) ? " " : "", e->tokens[t]);
            if (n < 0) break;
            written += (size_t) n;
        }
        if (written + 1 < out_size) {
            n = snprintf(out + written, out_size - written, ")");
            if (n < 0) break;
            written += (size_t) n;
        }
    }
    return out;
}

/* ------------------------------------------------------------------- parse --- */

/*
 * Parse a Permissions-Policy header value into a feature map. Accepts an
 * optional `Permissions-Policy:` prefix. Returns false when the syntax is
 * malformed (bad directive, missing allowlist, bad token).
 */
bool parse_permissions_policy(const char *header, feature_map *out)
{
    char buf[4096];
    char *cleaned, *directive, *save = NULL;

    if (out == NULL) {
        return false;
    }
    out->count = 0;

    cleaned = strip_header_prefix(header, buf, sizeof buf);
    if (cleaned[0] == '\0') {
        return false;
    }

    for (directive = strtok_r(cleaned, ",", &save);
         directive != NULL;
         directive = strtok_r(NULL, ",", &save)) {
        char *name, *value, *eq;
        const char *tokens[MAX_TOKENS];
        char token_store[MAX_TOKENS][MAX_TOKEN_LEN];
        size_t token_count = 0;

        directive = trim(directive);
        if (directive[0] == '\0') {
            return false; /* stray comma */
        }

        eq = strchr(directive, '=');
        if (eq == NULL || eq == directive) {
            return false; /* not `feature=allowlist` */
        }
        *eq = '\0';
        name  = trim(directive);
        value = trim(eq + 1);

        if (!is_feature_name(name)) {
            return false;
        }

        /* `*` and bare `self` are shorthands for a one-token allowlist. */
        if (strcmp(value, "*") == 0 || strcmp(value, "self") == 0) {
            const char *one[1] = { value };
            if (!feature_map_set(out, name, one, 1)) {
                return false;
            }
            continue;
        }

        {
            size_t vlen = strlen(value);
            if (vlen < 2 || value[0] != '(' || value[vlen - 1] != ')') {
                return false;
            }
            value[vlen - 1] = '\0';
            value = trim(value + 1);
        }

        if (value[0] == '\0') {
            /* `()` = the feature is disabled outright. */
            if (!feature_map_set(out, name, NULL, 0)) {
                return false;
            }
            continue;
        }

        {
            char *token, *tsave = NULL;
            for (token = strtok_r(value, " \t\r\n", &tsave);
                 token != NULL;
                 token = strtok_r(NULL, " \t\r\n", &tsave)) {
                if (token_count >= MAX_TOKENS) {
                    return false;
                }
                snprintf(token_store[token_count], MAX_TOKEN_LEN, "%s", token);
                unquote(token_store[token_count]);
                if (!is_allowlist_token(token_store[token_count])) {
                    return false;
                }
                tokens[token_count] = token_store[token_count];
                token_count++;
            }
        }
        if (!feature_map_set(out, name, tokens, token_count)) {
            return false;
        }
    }
    return true;
}

/* ---------------------------------------------------------------- validate --- */

static void push_error(validation_result *result, const char *fmt, ...)
{
    va_list args;

    if (result->error_count >= MAX_ERRORS) {
        return;
    }
    va_start(args, fmt);
    vsnprintf(result->errors[result->error_count], MAX_ERROR_LEN, fmt, args);
    va_end(args);
    result->error_count++;
}

/* Syntax-check a header the same way parse_permissions_policy() does, but
 * collecting a message per problem instead of bailing at the first. */
validation_result validate_permissions_policy(const char *header)
{
    validation_result result = { true, {{0}}, 0 };
    char buf[4096];
    char *cleaned, *directive, *save = NULL;
    int index = 0;

    cleaned = strip_header_prefix(header, buf, sizeof buf);
    if (cleaned[0] == '\0') {
        push_error(&result, "Header is empty.");
        result.valid = false;
        return result;
    }

    for (directive = strtok_r(cleaned, ",", &save);
         directive != NULL;
         directive = strtok_r(NULL, ",", &save), index++) {
        char *name, *value, *eq;
        char where[32];

        snprintf(where, sizeof where, "Directive %d", index + 1);
        directive = trim(directive);

        if (directive[0] == '\0') {
            push_error(&result, "%s: empty (stray comma?).", where);
            continue;
        }
        eq = strchr(directive, '=');
        if (eq == NULL || eq == directive) {
            push_error(&result, "%s: expected `feature=allowlist`, got `%s`.", where, directive);
            continue;
        }
        *eq = '\0';
        name  = trim(directive);
        value = trim(eq + 1);

        if (!is_feature_name(name)) {
            push_error(&result, "%s: `%s` is not a valid feature name.", where, name);
            continue;
        }
        if (strcmp(value, "*") == 0 || strcmp(value, "self") == 0) {
            continue;
        }
        {
            size_t vlen = strlen(value);
            if (vlen < 2 || value[0] != '(' || value[vlen - 1] != ')') {
                push_error(&result,
                           "%s: `%s` allowlist must be `*`, `self`, or `(...)` - got `%s`.",
                           where, name, value);
                continue;
            }
            value[vlen - 1] = '\0';
            value = trim(value + 1);
        }
        if (value[0] == '\0') {
            continue; /* () = disabled, valid */
        }
        {
            char *raw, *tsave = NULL;
            for (raw = strtok_r(value, " \t\r\n", &tsave);
                 raw != NULL;
                 raw = strtok_r(NULL, " \t\r\n", &tsave)) {
                char token[MAX_TOKEN_LEN];

                snprintf(token, sizeof token, "%s", raw);
                unquote(token);
                if (!is_allowlist_token(token)) {
                    push_error(&result, "%s: `%s` has an invalid allowlist token `%s`.",
                               where, name, raw);
                }
            }
        }
    }

    result.valid = result.error_count == 0;
    return result;
}

/* ------------------------------------------------------------------- score --- */

/*
 * Privacy score 0-100: how much of the catalogue the policy locks down. Each
 * feature is weighted by privacy impact (high 3, medium 2, low 1). Disabled
 * `()` earns full credit, `self` or an origin list half, and `*` or "absent
 * from the policy" none (the browser default stays in force).
 */
int privacy_score(const feature_map *map)
{
    double earned = 0.0, possible = 0.0;

    for (size_t i = 0; i < FEATURE_COUNT; i++) {
        const feature_info *feature = &FEATURES[i];
        const policy_entry *entry;
        double w;

        switch (feature->impact) {
            case IMPACT_HIGH:   w = 3.0; break;
            case IMPACT_MEDIUM: w = 2.0; break;
            default:            w = 1.0; break;
        }
        possible += w;

        entry = feature_map_get(map, feature->name);
        if (entry == NULL) {
            continue; /* browser default — no credit */
        }
        if (entry->token_count == 0) {
            earned += w; /* () disabled */
        } else if (!(entry->token_count == 1 && strcmp(entry->tokens[0], "*") == 0)) {
            earned += w / 2.0; /* self / origin list */
        }
    }

    if (possible == 0.0) {
        return 0;
    }
    /* JS Math.round: halves go up. */
    return (int) ((earned / possible) * 100.0 + 0.5);
}

/* -------------------------------------------------------------------- demo --- */

int main(void)
{
    feature_map map = {0};
    feature_map parsed = {0};
    char header[1024];
    validation_result check;

    /* A locked-down starter policy. */
    const char *self_only[1]  = { "self" };
    const char *everywhere[1] = { "*" };
    const char *partners[2]   = { "self", "https://widgets.example.com" };

    feature_map_set(&map, "geolocation", self_only, 1);
    feature_map_set(&map, "camera", NULL, 0);          /* disabled */
    feature_map_set(&map, "microphone", NULL, 0);      /* disabled */
    feature_map_set(&map, "usb", NULL, 0);             /* disabled */
    feature_map_set(&map, "autoplay", everywhere, 1);  /* allowed everywhere */
    feature_map_set(&map, "payment", partners, 2);

    build_permissions_policy(&map, header, sizeof header);
    printf("Permissions-Policy: %s\n\n", header);
    printf("privacy score: %d/100\n\n", privacy_score(&map));

    /* Round-trip: the built header parses back to an equivalent map. */
    if (parse_permissions_policy(header, &parsed)) {
        printf("parsed %zu directives, score %d/100\n\n", parsed.count, privacy_score(&parsed));
    } else {
        printf("parse failed\n\n");
    }

    /* Validation reports every problem, not just the first. */
    check = validate_permissions_policy("geolocation=(self), camera, usb=(ftp://nope), =broken");
    printf("valid: %s\n", check.valid ? "yes" : "no");
    for (size_t i = 0; i < check.error_count; i++) {
        printf("  - %s\n", check.errors[i]);
    }

    return EXIT_SUCCESS;
}

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 →