BBC's guide to development
  • General

    • About
    • Tools
    • Git(hub)
    • Showpad
    • Hosting
    • Maintenance
    • Security
    • Go live checklist
  • Front-end development

    • Bundlers
    • CSS/SCSS
    • Javascript
    • Vue
    • PHP
    • Mails
    • Dev Faq
  • Functions
  • Mixins
  • General

    • OOP Structure
  • Component Classes

    • Accordion
    • App
    • Component
    • HighwayApp
    • Popup
    • PNG Sequencer
    • Tab
  • Manager Classes

    • BountListenerMgr
    • Cache
    • Configuration
    • InViewStateMgr
    • Instance Manager
    • Event dispatcher
  • Factories

    • SwiperFactory
  • PDF

    • AssetLoader
    • BasePdfDoc
    • TemplatePdfDoc
    • CustomPdfDoc
  • Utility functions

    • canvas
    • Connection Status
    • css
    • dev
    • placeholder
    • dom
    • fetch
    • json
    • object
    • scroll
    • scrollbar
    • spreadsheets
    • string
    • url
  • General

    • ComponentMgr
    • ThreeJsViewer
  • Components

    • ComponentMgr
    • GltfModel
    • Snappable
    • Socket
    • ThreeJsViewer
    • ThreeJsViewerCamera
  • Loaders

    • ConfigurationSerializer
    • GltfBlockParser
  • Utils

    • CanvasInputAdapter
    • CollisionManager
    • SocketGridExpander
    • blender
    • headless
  • General

    • Troubleshooting
    • Legacy
  • Components

    • AssetBar
    • ConfigGenerator
    • ShowpadApp
  • Managers

    • Assets
    • AppsDb
    • Config
  • Utils

    • Connection Status
    • general
    • showpad-interactive
    • showpad-upload
  • Components

    • Accordion
    • BackButton
    • Breadcrumb
    • ByltButton
    • Hamburger
    • Icon
    • Logo
    • Loader
    • Modal
    • Popup
    • Prompt
    • ProgressBar
    • TextLoader
  • Composables

    • useDebugMode
    • useConnectionStatus
  • Utils

    • dom
    • props
  • General

    • General
    • Tracking
  • Components

    • Accordion
    • ActionButton
    • AssetItem
    • AssetList
    • BackButton
    • ConfigGenButton
    • Logo
    • Media
    • Modal
    • Popup
    • Prompt
    • SPButton
    • SPRouterView
    • SPTrackedRouterLink
    • TextLoader
    • View
  • Composables

    • useConnectionStatus
  • Stores

    • useAppsDbStore
    • useBreadcrumbStore
    • useShowpadAPIStore
    • useShowpadSDKStore
    • useSpConfigStore
    • useSpStore
    • useSpTrackingStore
  • The New Kit

    • General
    • Installation & Usage
    • ACF Blocks
    • PHPCS
    • Functions
    • Vite
    • WP Config
    • Staging Deployment
  • Best Practices

    • Page Structure
    • Fonts/Typography
  • Todo
GitHub
  • General

    • About
    • Tools
    • Git(hub)
    • Showpad
    • Hosting
    • Maintenance
    • Security
    • Go live checklist
  • Front-end development

    • Bundlers
    • CSS/SCSS
    • Javascript
    • Vue
    • PHP
    • Mails
    • Dev Faq
  • Functions
  • Mixins
  • General

    • OOP Structure
  • Component Classes

    • Accordion
    • App
    • Component
    • HighwayApp
    • Popup
    • PNG Sequencer
    • Tab
  • Manager Classes

    • BountListenerMgr
    • Cache
    • Configuration
    • InViewStateMgr
    • Instance Manager
    • Event dispatcher
  • Factories

    • SwiperFactory
  • PDF

    • AssetLoader
    • BasePdfDoc
    • TemplatePdfDoc
    • CustomPdfDoc
  • Utility functions

    • canvas
    • Connection Status
    • css
    • dev
    • placeholder
    • dom
    • fetch
    • json
    • object
    • scroll
    • scrollbar
    • spreadsheets
    • string
    • url
  • General

    • ComponentMgr
    • ThreeJsViewer
  • Components

    • ComponentMgr
    • GltfModel
    • Snappable
    • Socket
    • ThreeJsViewer
    • ThreeJsViewerCamera
  • Loaders

    • ConfigurationSerializer
    • GltfBlockParser
  • Utils

    • CanvasInputAdapter
    • CollisionManager
    • SocketGridExpander
    • blender
    • headless
  • General

    • Troubleshooting
    • Legacy
  • Components

    • AssetBar
    • ConfigGenerator
    • ShowpadApp
  • Managers

    • Assets
    • AppsDb
    • Config
  • Utils

    • Connection Status
    • general
    • showpad-interactive
    • showpad-upload
  • Components

    • Accordion
    • BackButton
    • Breadcrumb
    • ByltButton
    • Hamburger
    • Icon
    • Logo
    • Loader
    • Modal
    • Popup
    • Prompt
    • ProgressBar
    • TextLoader
  • Composables

    • useDebugMode
    • useConnectionStatus
  • Utils

    • dom
    • props
  • General

    • General
    • Tracking
  • Components

    • Accordion
    • ActionButton
    • AssetItem
    • AssetList
    • BackButton
    • ConfigGenButton
    • Logo
    • Media
    • Modal
    • Popup
    • Prompt
    • SPButton
    • SPRouterView
    • SPTrackedRouterLink
    • TextLoader
    • View
  • Composables

    • useConnectionStatus
  • Stores

    • useAppsDbStore
    • useBreadcrumbStore
    • useShowpadAPIStore
    • useShowpadSDKStore
    • useSpConfigStore
    • useSpStore
    • useSpTrackingStore
  • The New Kit

    • General
    • Installation & Usage
    • ACF Blocks
    • PHPCS
    • Functions
    • Vite
    • WP Config
    • Staging Deployment
  • Best Practices

    • Page Structure
    • Fonts/Typography
  • Todo
GitHub
  • Security Guide

Security Guide

This document outlines the security measures implemented across our web projects. It serves as a reference for developers and as a handoff document for security audits.


1. Content Security Policy (CSP)

What is CSP

A Content Security Policy is an HTTP response header that tells the browser which sources are allowed to load scripts, styles, fonts, images and other resources. It is one of the most effective defences against Cross-Site Scripting (XSS) attacks.

Where to set it

The CSP header must be set in PHP — not in .htaccess. This is required because the nonce (see below) is dynamically generated per request and cannot be done in a static .htaccess file.

Remove any existing Header set Content-Security-Policy line from .htaccess when moving to PHP.

Generating a nonce

A nonce is a unique, random value generated on every page request. It is used to whitelist specific inline scripts and styles without opening up unsafe-inline for everything.

Place this at the very top of your index.php, before any HTML output:

<?php
// Must be the absolute first line — no blank lines or spaces above this
$nonce = base64_encode(random_bytes(16));

header("Content-Security-Policy: " .
    "script-src 'self' 'nonce-{$nonce}' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; " .
    "style-src 'self' 'nonce-{$nonce}' https://fonts.googleapis.com; " .
    "object-src 'none'; " .
    "base-uri 'self';"
);
?>
<!DOCTYPE html>
<html>
...

⚠️ Any whitespace or output before <?php will cause PHP to silently skip the header() call. You'll see a Cannot modify header information — headers already sent warning if this happens.

Applying the nonce to inline tags

Every inline <script> and <style> block must include the nonce attribute:

<!-- Inline script -->
<script nonce="<?= $nonce ?>">
    window.__CONFIG__ = { env: 'production' };
</script>

<!-- Inline style -->
<style nonce="<?= $nonce ?>">
    :root { --brand-color: #0055ff; }
</style>

External files loaded via src or href do not need a nonce — they are covered by the domain allowlist:

<!-- No nonce needed — domain is in script-src -->
<script src="https://cdn.jsdelivr.net/npm/somelib/dist/somelib.min.js"></script>

<!-- No nonce needed — domain is in style-src -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter">

Directives to avoid

DirectiveRiskAlternative
unsafe-inlineAllows any inline script or styleUse nonces or hashes
unsafe-hashesWhitelists inline event handlersMove to addEventListener()
unsafe-evalAllows eval() and new Function()Refactor code to avoid eval

Inline event handlers

unsafe-hashes is typically needed when HTML contains inline event handlers like onclick="...". The fix is to move these into external JS files:

<!-- ❌ Before -->
<button onclick="doSomething()">Click</button>

<!-- ✅ After -->
<button id="my-btn">Click</button>
// In your external JS file
document.getElementById('my-btn').addEventListener('click', doSomething);

GTM & third-party scripts

Google Tag Manager injects inline scripts into the page. To cover these with your nonce:

  1. GTM → Admin → Container Settings
  2. Enable "Add nonce to dynamically injected scripts"
  3. Save and publish the container

Scripts loaded through GTM (e.g. Cookiebot, analytics tags) will inherit the nonce automatically.

For Cookiebot loaded directly (not via GTM), add data-nonce to the script tag:

<script
    id="Cookiebot"
    src="https://consent.cookiebot.com/uc.js"
    data-cbid="YOUR-CBID"
    data-nonce="<?= $nonce ?>"
    nonce="<?= $nonce ?>">
</script>

Validating your CSP

  • DevTools → Network → page request → Response Headers → Content-Security-Policy
  • Console → any blocked resource shows a red Refused to execute error
  • CSP Evaluator → paste your policy at https://csp-evaluator.withgoogle.com/
  • Report-Only mode → use Content-Security-Policy-Report-Only to log violations without blocking, useful for testing before enforcing

frame-ancestors exception

If a page needs to be embedded from an unknown or variable origin (e.g. a kiosk or booth deployment), frame-ancestors * can be set in the CSP. This must be a documented, client-approved exception:

"frame-ancestors *;"
// Document reason: required for kiosk/booth deployment — approved by client on [date]

2. Subresource Integrity (SRI)

What is SRI

SRI ensures that externally loaded static resources (scripts, stylesheets) have not been tampered with. The browser computes a hash of the fetched file and compares it against the hash in the HTML. If they don't match, the resource is blocked.

When to use it

SRI should be applied to any static, versioned resource loaded from a CDN. It should not be applied to dynamic scripts that are continuously updated by the vendor (see exemptions below).

How to add it

Generate the hash at https://www.srihash.org/ and add integrity and crossorigin attributes:

<!-- Stylesheet with SRI -->
<link
    rel="stylesheet"
    href="https://cdn.jsdelivr.net/npm/@fancyapps/ui@4.0/dist/fancybox.css"
    integrity="sha256-3e3RCRzozjqLEcmEZnSoKv/VL6BFlGxofHUWCZ3BXGA="
    crossorigin="anonymous">

<!-- Script with SRI -->
<script
    src="https://cdnjs.cloudflare.com/ajax/libs/slim-select/1.27.1/slimselect.min.js"
    integrity="sha256-HASH_HERE"
    crossorigin="anonymous">
</script>

Checking for missing SRI

Use the sri-check CLI tool to scan a page for any resources missing integrity attributes:

# Install
pip install sri-check

# Run against a URL
sri-check https://yoursite.com/page/

Any flagged <script> or <link> tag should either have SRI added or be documented as an exemption.

Exemptions

Dynamic third-party scripts cannot have SRI because their content changes frequently. These should instead be covered by domain allowlisting in the CSP.

ScriptReason for exemption
Google reCAPTCHADynamically updated by Google
Google Tag ManagerDynamically updated by Google
Google Analytics / gtag.jsDynamically updated by Google
HotjarDynamically updated by vendor

Maintaining SRI hashes

SRI hashes are tied to a specific file version. If the library is updated, the hash must be updated too.

Process when updating a CDN library:

  1. Update the version number in the href or src URL
  2. Run sri-check against the page or generate a new hash at https://www.srihash.org/
  3. Update the integrity attribute in the HTML
  4. Test in the browser to confirm no SRI errors in the console

Tip: Use pinned, exact version numbers in CDN URLs (e.g. @4.0.12 not @4) so the file never silently changes under you.


3. Security Headers

The following headers should be set in .htaccess. The CSP is the exception — it belongs in PHP (see section 1).

# Security Headers
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"

Place these above the <IfModule mod_rewrite.c> block.

What each header does

HeaderPurpose
X-Content-Type-Options: nosniffPrevents browsers from MIME-sniffing a response away from the declared content type
X-Frame-Options: SAMEORIGINBlocks the page from being embedded in an iframe on external domains (clickjacking protection)
Referrer-PolicyControls how much referrer information is sent with requests
Permissions-PolicyDisables browser features (camera, mic, geolocation) that the site doesn't use
Strict-Transport-SecurityForces HTTPS for 12 months — only add if the site is fully on HTTPS

Validating headers

Check your score at https://securityheaders.com — paste your URL and it grades all headers. Target Grade A as a minimum.


4. Input Validation & Sanitization

Always sanitize and validate user input server-side. Never rely on client-side validation alone.

Sanitization

Strip or encode unexpected characters before using input:

$email      = filter_var($email, FILTER_SANITIZE_EMAIL);
$url        = filter_var($url, FILTER_SANITIZE_URL);
$text_field = strip_tags($text_field);

Validation

Confirm the value is the expected format after sanitizing:

if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
    // reject — not a valid email
}

if (!filter_var($url, FILTER_VALIDATE_URL) || !str_contains($url, 'expecteddomain.com')) {
    // reject — not a valid or expected URL
}

Output escaping

Any value echoed back into HTML must be wrapped in htmlspecialchars() at the point of output, regardless of earlier sanitization:

echo htmlspecialchars($user_input, ENT_QUOTES, 'UTF-8');

This is the last line of defence against XSS and should always be applied.

Sanitize vs validate

StepWhat it doesWhen
SanitizeCleans the value — removes/encodes unexpected charactersBefore storing or using input
ValidateConfirms it matches the expected formatAlways, after sanitizing

5. reCAPTCHA v3 — Server-side Verification

Loading reCAPTCHA on the frontend is not enough — the token must be verified server-side. Without this, the check can be bypassed entirely.

Frontend — capture the token

grecaptcha.ready(function() {
    grecaptcha.execute('YOUR_SITE_KEY', { action: 'submit' })
        .then(function(token) {
            document.getElementById('recaptcha_token').value = token;
        });
});
<input type="hidden" id="recaptcha_token" name="recaptcha_token">

Backend — verify with Google

$secret_key = 'YOUR_SECRET_KEY'; // from Google reCAPTCHA console
$token      = $_POST['recaptcha_token'] ?? '';

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://www.google.com/recaptcha/api/siteverify");
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
    'secret'   => $secret_key,
    'response' => $token
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response, true);

if (!$result['success'] || $result['score'] < 0.5) {
    http_response_code(403);
    exit('reCAPTCHA verification failed.');
}

// ✅ Passed — process the form

Score reference

ScoreMeaning
1.0Very likely human
0.5Recommended minimum threshold
0.0Very likely bot

Raise the threshold to 0.7 if spam is getting through.


6. Tools Reference

ToolPurposeURL
sri-checkScan pages for missing SRI attributeshttps://github.com/4ARMED/sri-check
SRI Hash GeneratorGenerate integrity hashes for CDN resourceshttps://www.srihash.org/
CSP EvaluatorAnalyse and validate CSP policyhttps://csp-evaluator.withgoogle.com/
Security HeadersGrade HTTP response headershttps://securityheaders.com

7. Checklist

Use this checklist for every new or audited project.

CSP

SRI

Security Headers

Forms & Input

Edit this page
Last Updated: 4/27/26, 12:56 PM
Contributors: Nicolas Jaenen