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 - pure Permissions-Policy header builder / parser.
// Zero dependencies.
//
// Language: C# 12 / .NET 8 (standard library only)
// Source: CosmoDev polyglot showcase port of the Permissions Policy 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 map of feature -> allowlist tokens; features absent
// from the map are absent from the header (browser default applies).
using System.Text.RegularExpressions;
public enum PrivacyImpact
{
High,
Medium,
Low,
}
/// <summary>Catalog entry for one browser feature the policy can control.</summary>
/// <param name="Name">The directive token used in the header, e.g. "geolocation".</param>
/// <param name="Description">What the feature grants, in one line.</param>
/// <param name="Impact">How much the feature matters for privacy.</param>
/// <param name="DefaultBrowserBehavior">What browsers do when the feature is absent from the policy.</param>
public sealed record FeatureInfo(
string Name,
string Description,
PrivacyImpact Impact,
string DefaultBrowserBehavior);
public static class PermissionsPolicy
{
public static readonly IReadOnlyList<FeatureInfo> Features = new FeatureInfo[]
{
// --- high privacy impact ---
new("camera", "Access the device camera for photos / video calls.", PrivacyImpact.High, "Same-origin only; prompts the user."),
new("microphone", "Capture audio from the device microphone.", PrivacyImpact.High, "Same-origin only; prompts the user."),
new("geolocation", "Read the precise GPS location of the visitor.", PrivacyImpact.High, "Same-origin only; prompts the user."),
new("display-capture", "Screen / window sharing via getDisplayMedia.", PrivacyImpact.High, "Same-origin only; prompts the user."),
new("idle-detection", "Detects when the user is away from the device - reveals usage patterns.", PrivacyImpact.High, "Disabled; prompts the user."),
new("serial", "Talk to serial devices (Arduinos, POS terminals) over a physical port.", PrivacyImpact.High, "Disabled; prompts the user."),
new("usb", "WebUSB - direct access to connected USB devices.", PrivacyImpact.High, "Disabled; prompts the user."),
new("hid", "Human Interface Devices - raw access to unusual keyboards, gamepads, sensors.", PrivacyImpact.High, "Disabled; prompts the user."),
new("xr-spatial-tracking", "Tracks head / hand position in WebXR sessions.", PrivacyImpact.High, "Same-origin only; prompts the user."),
// --- medium privacy impact ---
new("accelerometer", "Device motion sensor - can fingerprint and infer behaviour.", PrivacyImpact.Medium, "Same-origin only."),
new("ambient-light-sensor", "Reads ambient light level around the device.", PrivacyImpact.Medium, "Same-origin only."),
new("battery", "Battery Status API - a classic fingerprinting vector.", PrivacyImpact.Medium, "Same-origin only."),
new("gyroscope", "Device orientation sensor - fingerprinting and behaviour inference.", PrivacyImpact.Medium, "Same-origin only."),
new("magnetometer", "Compass readings - can leak details of the user's surroundings.", PrivacyImpact.Medium, "Same-origin only."),
new("keyboard-map", "Reads the physical keyboard layout - a small but real fingerprint.", PrivacyImpact.Medium, "Same-origin only."),
new("gamepad", "Enumerates connected controllers and their button state.", PrivacyImpact.Medium, "Same-origin only."),
new("midi", "Web MIDI - access to attached music hardware.", PrivacyImpact.Medium, "Same-origin only; prompts the user."),
new("payment", "Payment Request API - can expose stored payment handles.", PrivacyImpact.Medium, "Same-origin only."),
new("publickey-credentials-get", "WebAuthn credential requests.", PrivacyImpact.Medium, "Same-origin only."),
new("screen-wake-lock", "Keeps the screen awake - drains battery and signals intent.", PrivacyImpact.Medium, "Same-origin only."),
new("speaker-selection", "Enumerates and switches audio output devices.", PrivacyImpact.Medium, "Same-origin only."),
new("web-share", "Invokes the OS share sheet with chosen content.", PrivacyImpact.Medium, "Same-origin only."),
new("encrypted-media", "DRM (EME) - playback identity can be correlated.", PrivacyImpact.Medium, "Same-origin only."),
new("document-domain", "Let frames relax the same-origin policy via document.domain.", PrivacyImpact.Medium, "Allowed in same-origin pages; deprecated."),
// --- low privacy impact ---
new("autoplay", "Autoplay media with/without sound - not a data leak, an annoyance knob.", PrivacyImpact.Low, "Muted autoplay allowed; audible blocked."),
new("cross-origin-isolated", "COOP/COEP isolation for SharedArrayBuffer - hardens the page.", PrivacyImpact.Low, "Not isolated."),
new("fullscreen", "Element.requestFullscreen().", PrivacyImpact.Low, "Same-origin only."),
new("navigation-override", "Lets a frame intercept its own top-level navigations.", PrivacyImpact.Low, "Disabled."),
new("picture-in-picture", "Floating always-on-top video window.", PrivacyImpact.Low, "Same-origin only."),
};
private static readonly Regex FeatureNameRe = new("^[a-z][a-z0-9-]*$", RegexOptions.Compiled);
private static readonly Regex OriginRe = new("^(?:https?://|https?:)[\\w.-]+(?::\\d+)?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex HeaderPrefixRe = new("^permissions-policy\\s*:\\s*", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex QuotedTokenRe = new("^\"(.*)\"$", RegexOptions.Compiled);
private static string FormatAllowlist(string[] tokens) =>
tokens.Length == 1 && tokens[0] == "*"
? "*"
: $"({string.Join(' ', tokens)})";
/// <summary>Assemble a Permissions-Policy header value from a feature map.</summary>
public static string BuildPermissionsPolicy(IReadOnlyDictionary<string, string[]> features) =>
string.Join(", ", features.Keys
.OrderBy(n => n, StringComparer.Ordinal)
.Select(name => $"{name}={FormatAllowlist(features[name])}"));
/// <summary>
/// Parse a Permissions-Policy header value back into a feature map. Accepts
/// an optional "Permissions-Policy:" prefix. Returns null when the syntax
/// is malformed (bad directive, missing allowlist, bad token).
/// </summary>
public static Dictionary<string, string[]>? ParsePermissionsPolicy(string header)
{
string cleaned = HeaderPrefixRe.Replace(header.Trim(), "");
if (cleaned.Length == 0) return null;
var map = new Dictionary<string, string[]>();
foreach (string rawDirective in cleaned.Split(','))
{
string directive = rawDirective.Trim();
if (directive.Length == 0) return null;
int eq = directive.IndexOf('=');
if (eq <= 0) return null;
string name = directive[..eq].Trim();
string value = directive[(eq + 1)..].Trim();
if (!FeatureNameRe.IsMatch(name)) return null;
if (value is "*" or "self")
{
map[name] = new[] { value };
continue;
}
if (!value.StartsWith('(') || !value.EndsWith(')')) return null;
string inner = value[1..^1].Trim();
if (inner.Length == 0)
{
map[name] = Array.Empty<string>();
continue;
}
string[] tokens = Regex.Split(inner, "\\s+").Select(Unquote).ToArray();
foreach (string token in tokens)
{
if (token != "self" && token != "*" && !OriginRe.IsMatch(token)) return null;
}
map[name] = tokens;
}
return map;
}
/// <summary>Syntax-check a header the same way ParsePermissionsPolicy does, with messages.</summary>
public static (bool Valid, IReadOnlyList<string> Errors) ValidatePermissionsPolicy(string header)
{
var errors = new List<string>();
string cleaned = HeaderPrefixRe.Replace(header.Trim(), "");
if (cleaned.Length == 0) return (false, new[] { "Header is empty." });
int i = 0;
foreach (string rawDirective in cleaned.Split(','))
{
i++;
string directive = rawDirective.Trim();
string where = $"Directive {i}";
if (directive.Length == 0)
{
errors.Add($"{where}: empty (stray comma?).");
continue;
}
int eq = directive.IndexOf('=');
if (eq <= 0)
{
errors.Add($"{where}: expected `feature=allowlist`, got `{directive}`.");
continue;
}
string name = directive[..eq].Trim();
string value = directive[(eq + 1)..].Trim();
if (!FeatureNameRe.IsMatch(name))
{
errors.Add($"{where}: `{name}` is not a valid feature name.");
continue;
}
if (value is "*" or "self") continue;
if (!value.StartsWith('(') || !value.EndsWith(')'))
{
errors.Add($"{where}: `{name}` allowlist must be `*`, `self`, or `(...)` - got `{value}`.");
continue;
}
string inner = value[1..^1].Trim();
if (inner.Length == 0) continue; // () = disabled, valid
foreach (string raw in Regex.Split(inner, "\\s+"))
{
string token = Unquote(raw);
if (token != "self" && token != "*" && !OriginRe.IsMatch(token))
{
errors.Add($"{where}: `{name}` has an invalid allowlist token `{raw}`.");
}
}
}
return (errors.Count == 0, errors);
}
/// <summary>
/// Privacy score 0-100 for a policy: how much it locks down the catalog.
/// Each feature is weighted by privacy impact (high 3, medium 2, low 1).
/// A disabled () allowlist earns full credit, self or an origin list half,
/// * or "absent from the policy" none (the browser default stays in force).
/// </summary>
public static int PrivacyScore(IReadOnlyDictionary<string, string[]> features)
{
int Weight(PrivacyImpact impact) => impact switch
{
PrivacyImpact.High => 3,
PrivacyImpact.Medium => 2,
_ => 1,
};
double earned = 0;
double possible = 0;
foreach (FeatureInfo feature in Features)
{
int w = Weight(feature.Impact);
possible += w;
if (!features.TryGetValue(feature.Name, out string[]? allowlist)) continue;
if (allowlist.Length == 0) earned += w; // () disabled
else if (!(allowlist.Length == 1 && allowlist[0] == "*")) earned += w / 2.0; // self / origins
}
return (int)Math.Round(earned / possible * 100, MidpointRounding.AwayFromZero);
}
private static string Unquote(string token)
{
Match m = QuotedTokenRe.Match(token);
return m.Success ? m.Groups[1].Value : token;
}
}
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 →