Luminova Framework

PHP Luminova: Content Security Policy

Last updated: 2025-12-14 19:49:22

Easily add, change, or remove CSP directives to control what resources your web app can load, improving security against attacks like XSS.

The CSP class provides a flexible methods for Content-Security-Policy (CSP) in web applications. It allows developers to define and manage security directives to control which resources are allowed to load on a web page, helping prevent XSS attacks, data injection, and other malicious behaviors.

The class includes helper methods for commonly used CSP directives such as defaultSrc, scriptSrc, styleSrc, imgSrc, connectSrc, fontSrc, mediaSrc, objectSrc, and frameSrc. It also supports features like inline script/style nonces, hashes, report-uri, and report-to directives for violation reporting.


Examples

Building CSP Directives (Basic Way)

This example shows how to manually add each Content Security Policy (CSP) rule.

use Luminova\Security\CSP;

$policy = (new CSP())
    ->add('default-src', ["'self'"]) 
    ->add('script-src', ["'self'", 'https://scripts.example.com'])
    ->add('img-src', ['*'])
    ->add('style-src', ["'self'", "'unsafe-inline'"])
    ->add('object-src', ["'none'"])
    ->add('script-src', "'nonce-XYZ...'") // Manually add a nonce
    ->reportOnly(false)                   // Enforce policy (not report-only)
    ->build();

This version uses helper methods. It’s shorter, safer, and harder to mess up.

use Luminova\Security\CSP;

$policy = (new CSP())
    ->defaultSrc(['self', 'https://cdn.example.com'])
    ->scriptSrc(['self'])
    ->styleSrc(['self', 'https://fonts.googleapis.com'])
    ->imgSrc(['self', 'data:'])
    ->connectSrc(['self', 'https://api.example.com'])
    ->addNonce('script-src') // Auto-generates a secure nonce
    ->reportOnly(false)
    ->build();

A nonce is a one-time token generated per request.If the nonce matches, the browser runs the inline script or style. If not, it’s blocked. Simple and effective.

use Luminova\Security\CSP;

$csp = (new CSP())
    ->scriptSrc(['self'])
    ->styleSrc(['self'])
    ->addNonce('script-src')
    ->addNonce('style-src');

$csp->sendHeader();

What’s happening:

  • A fresh nonce is created on every request.
  • The nonce is attached to the CSP header.
  • Only inline code with the same nonce is allowed to run.
  • No need for 'unsafe-inline' (which defeats the point of CSP).

Using Nonces in Templates

Just print the nonce where the inline code lives.

<style nonce="<?= $csp->getNonce('style-src'); ?>">
    body { background: #f5f5f5; }
</style>

<script nonce="<?= $csp->getNonce('script-src'); ?>">
    console.log("Inline script executed safely with CSP nonce");
</script>

Note:

  • One nonce per directive per request.
  • If the nonce is missing or wrong, the browser blocks the code.
  • Works best when templates are rendered server-side.

Using Hashes for Inline Scripts

Hashes are an alternative to nonces.Instead of trusting a request, the browser trusts the exact script content.

Use hashes when:

  • The inline script never changes.
  • You want aggressive caching.
  • You don’t control template rendering per request.

Manually Adding a Script Hash

use Luminova\Security\CSP;

$csp = (new CSP())
    ->scriptSrc(['self'])
    ->add('script-src', 'sha256-AbCdEfGhIjK...');

Generating a Hash Yourself

$hash = base64_encode(
    hash('sha256', 'console.log("Hello World");', true)
);

$csp = (new CSP())
    ->scriptSrc(['self'])
    ->addHash('script-src', 'sha256', $hash);

The script must match exactly.One extra space and the browser says “no”.


Auto-Generated Hash

If you don’t want to generate the hash manually:

$csp = (new CSP())
    ->scriptSrc(['self'])
    ->addHash('script-src', 'sha256');

$hash = $csp->getHash('script-src');

This allows the CSP helper to generate and attach a hash automatically.


Nonce vs Hash (Quick Comparison)

Use CaseChoose
Dynamic pagesNonce
Static inline scriptHash
Per-request securityNonce
Long-term cachingHash
  • Nonces are simpler to implement and provide strong security for most applications.
  • Hashes offer precise control but require strict matching and careful maintenance.
  • 'unsafe-inline' should be avoided in production environments and used only for testing or demonstration purposes.

Securing a Webpage with CSP

This example shows how to secure a page using Content Security Policy (CSP) in an HTTP controller.

What this setup does:

  • Inline scripts and styles use generated nonces.
  • The browser blocks any script or style not allowed by CSP.
  • You can switch to reportOnly(true) to log violations without blocking them.

Controller Example

namespace App\Controllers\Http;

use Luminova\Security\CSP;
use Luminova\Base\Controller;
use Luminova\Attributes\Prefix;
use Luminova\Attributes\Route;

#[Prefix('/dashboard/(:base)')]
class DashboardController extends Controller
{
    private CSP $csp;

    protected function onCreate(): void
    {
        $this->csp = (new CSP())
            ->defaultSrc(['self', 'https://cdn.example.com'])
            ->scriptSrc(['self'])
            ->styleSrc(['self', 'https://fonts.googleapis.com'])
            ->imgSrc(['self', 'data:'])
            ->addNonce('script-src')   // Allow inline scripts
            ->addNonce('style-src');    // Allow inline styles
    }

    #[Route('/dashboard/')]
    public function dashboard(): int
    {
        $headers = $this->csp->getHeaders();

        // Enqueue CSP header
        $this->tpl->headers($headers);

        // Render view and expose CSP to template
        return $this->view('dashboard', [
            'csp' => $this->csp
        ]);
    }
}

Notes for beginners:

  • CSP is created once per request.
  • Nonces are generated automatically.
  • The CSP header is queue for rendering the page template.
  • The CSP object is passed to the template so nonces can be reused safely.

Using CSP in Templates

Inline scripts and styles must include the correct nonce or the browser will block them.

Template file:/resources/Views/dashboard.php

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Dashboard</title>

    <!-- Optional: CSP meta tag fallback -->
    <?= $csp->getMetaTag('csp-meta'); ?>

    <!-- Inline style with nonce -->
    <style nonce="<?= $csp->getNonce('style-src'); ?>">
        body {
            background-color: #f5f5f5;
            font-family: Arial, sans-serif;
        }
        h1 {
            color: #333;
        }
    </style>

    <!-- External stylesheet -->
    <link rel="stylesheet"
          href="https://fonts.googleapis.com/css?family=Roboto:400,700">
</head>
<body>
    <h1>Welcome to the Dashboard</h1>

    <!-- Inline script with nonce -->
    <script nonce="<?= $csp->getNonce('script-src'); ?>">
        console.log('Dashboard inline script executed safely');
    </script>

    <!-- External script -->
    <script src="https://cdn.example.com/dashboard.js"></script>
</body>
</html>

Securing an API Response with CSP (Report-Only Mode)

For APIs, CSP is mainly used for monitoring, not blocking.reportOnly(true) lets you see violations without breaking clients.

Use this when:

  • You want visibility into what browsers would block.
  • You’re rolling out CSP gradually.
  • You don’t serve HTML but still want security telemetry.

API Controller Example

namespace App\Controllers\Http;

use Luminova\Security\CSP;
use Luminova\Base\Controller;
use Luminova\Attributes\Prefix;
use Luminova\Attributes\Route;
use function Luminova\Funcs\response;

#[Prefix('/api/(:base)')]
class RestController extends Controller
{
    private CSP $csp;

    protected function onCreate(): void
    {
        $this->csp = (new CSP())
            ->defaultSrc(['self'])
            ->scriptSrc(['self'])
            ->connectSrc(['self'])
            ->reportOnly(true)
            ->reportUri('/csp/report')   // Legacy reporting
            ->reportTo('csp-group', '/csp/report');    // Modern reporting
    }

    #[Route('/api/list')]
    public function list(): int
    {
        // Get CSP headers
        $headers = $this->csp->getHeaders();

        // Return secured JSON response
        return response(headers: $headers)->json([
            'status'  => 'success',
            'message' => 'API response secured with CSP (report-only)',
        ]);
    }
}

How This Works

  • The CSP header is sent with the API response.
  • Browsers do not block anything in report-only mode.
  • Any policy violation is reported to the configured endpoint.
  • API consumers continue working normally.

This makes CSP safe to deploy even on live APIs.


Configuring a CSP Reporting Endpoint

A CSP reporting endpoint is just a normal HTTP endpoint that accepts POST requests containing JSON.Browsers send CSP violations there automatically.

Create a Reporting Endpoint (Controller)

This endpoint receives violation reports from the browser.

namespace App\Controllers\Http;

use Luminova\Base\Controller;
use Luminova\Attributes\Route;
use Luminova\Attributes\Prefix;
use function Luminova\Funcs\{response, logger};

#[Prefix('/csp/(:base)')]
class CspReportController extends Controller
{
    #[Route('/csp/report', methods: ['POST'])]
    public function report(): int
    {
        $payload = $this->request->getRaw();

        // Log or store the report
        logger('debug', json_decode($payload, true));

        // Browser does not care about the response body
        return response(status: 204)->send();
    }
}

If reporting is enabled, browsers send violations as a JSON POSTto your configured reporting endpoint.

Example violation payload:

{
  "csp-report": {
    "document-uri": "https://example.com/page",
    "blocked-uri": "https://evil.com/script.js",
    "violated-directive": "script-src 'self'",
    "original-policy": "default-src 'self'; script-src 'self'"
  }
}

Note:

  • Must accept POST
  • Must accept JSON
  • Response body is ignored (204 is ideal)
  • Do not throw errors here — browsers will retry