Receiving security header violation reports using PHP.


Last updated: November 25, 2017.

You can somewhat easily receive JSON-formatted security header violation reports by email using PHP.


1. Create a new virtual host solely for recieving reports.

Create a subdomain of your current domain, or use an entirely different domain for report receiving.

Using Apache, this is a decent configuration using a subdomain. It's configured as if you are using Let's Encrypt with OCSP stapling. Ordinary security headers can be set, but you must not pin certificates or enable reporting.

<IfModule mod_ssl.c>
<VirtualHost *:443>
    # Uncomment this to enable HTTP/2. You must have enabled the module.
    # Protocols h2 http/1.1

    # Enable TLS 1.2 only, with modern ciphers and generally good practices.
    SSLEngine On
    SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
    SSLCipherSuite ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256
    SSLHonorCipherOrder On
    SSLCompression Off
    SSLSessionTickets Off
    SSLOptions +StrictRequire   

    # Enable OSCP stapling.
    # You must have configured SSLStaplingCache in the global configuration file.
    SSLUseStapling On
    SSLStaplingResponderTimeout 5
    SSLStaplingReturnResponderErrors Off

    # Secuirty headers.
    # Enable restrictive CSP. 
    Header always set Content-Security-Policy "default-src 'none'; frame-ancestors 'none'; sandbox; upgrade-insecure-requests"

    # Enable HSTS
    Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"

    # Instruct browsers not to render your domain in frames, iframes, or objects.
    Header always set X-Frame-Options "DENY"

    # Instruct browsers not to send the Referer header.
    Header always set Referrer-Policy "no-referrer"

    # Instruct browsers not to MIME sniff.
    Header always set X-Content-Type-Options "nosniff"

    # Enable XSS filtering, and block rendering of the page if an attack is detected.
    Header always set X-XSS-Protection "1; mode=block"

    # Opt-out of DNS prefetching.
    Header always set X-DNS-Prefetch-Control "off"

    # Ensure that all cookies have the secure flag set.
    Header edit Set-Cookie (?i)^(.*)(;\s*secure)??((\s*;)?(.*)) "$1; Secure$3$4"

    # Change this to your domain.
    ServerName report.example.com

    ServerAdmin admin@example.com
    DocumentRoot /var/www/report.example.com/html

    # Standard logging.
    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined
    LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\"" vhost_combined
    LogFormat "%v %h %l %u %t \"%r\" %>s %b" vhost_common

    # Standard Let's Encrypt configuration.
    SSLCertificateFile /etc/letsencrypt/live/report.example.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/report.example.com/privkey.pem

    # Uncomment this for my ECDSA configuration.
    # For SSLCertificateFile use the 0000_chain.pem with the highest number.
    # SSLCertificateFile /etc/letsencrypt/ecdsa/report.example.com/0001_chain.pem
    # SSLCertificateKeyFile /etc/letsencrypt/ecdsa/report.example.com/privkey.pem
</VirtualHost>
</IfModule>

2. Write your PHP

First, create your directory structure.

sudo mkdir -p /var/www/report.example.com/html/report-uri/

Create index.php.

sudo nano /var/www/report.example.com/html/report-uri/index.php

And then paste the PHP below into index.php, adding your to and from email addresses.

This PHP will accept any JSON data, and email it to a specified address.

<?php
    if($_SERVER['REQUEST_METHOD'] == 'POST'){
        http_response_code(204);

        $data = file_get_contents('php://input');

        if($data = json_decode($data)){
            $data = json_encode(
                $data,
                JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES
            );

            $to = ''; //Enter to email here.
            $email_subject = "Security violation.";
            $headers = "From: noreply@example.com\n"; //Enter from email here.
            $headers .= "Content-Type: text/plain;charset=utf-8";   
            mail($to, $email_subject, $data, $headers);
        }
    }
    else{
        http_response_code(405);
        header("Allow: POST");
        print "Method not allowed.";
        exit();
    }
?>

This requires you have PHP installed and setup, and a mail program like Sendmail. See here for instructions on configuring Sendmail to use TLS with Let's Encrypt.

Finally, set the correct permisions and (optionally) owner.

sudo chmod -R 755 /var/www/report.example.com/html/

sudo chown -R user: /var/www/report.example.com/html/


4. Implement the relevant security headers on your primary host/s.

The report-uri parameter is supported by Public-Key-Pins, Public-Key-Pins-Report-Only, Content-Security-Policy, Content-Security-Policy-Report-Only, X-XSS-Protection, and Expect-CT.


Public-Key-Pins-Report-Only

Update: This security header (and Public-Key-Pins) is being deprecated.


Public-Key-Pins can be dangerous, so I'll demonstrate Public-Key-Pins-Report-Only instead.

Public-Key-Pins-Report-Only will notify you of a violation, but will not interfere with the user at all.

For Let's Encrypt it's recommended you pin ISRG Root X1, Let’s Encrypt Authority X3, and Let’s Encrypt Authority X4.

To generate the hashes, run these three commands.

For ISRG Root X1:

curl https://letsencrypt.org/certs/isrgrootx1.pem | openssl x509 -pubkey | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64

For Let’s Encrypt Authority X3:

curl https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem | openssl x509 -pubkey | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64

And for Let’s Encrypt Authority X4:

curl https://letsencrypt.org/certs/lets-encrypt-x4-cross-signed.pem | openssl x509 -pubkey | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64

In your Apache VirtualHost directive, add the following line. You must replace hash0 - hash2 with the output of the above three commands (in any order).

Header always set Public-Key-Pins-Report-Only "max-age=0; pin-sha256=\"hash0"; pin-sha256=\"hash1"; pin-sha256=\"hash2"; report-uri=\"https://report.example.com/report-uri/\""

Note that the includeSubDomains option must not be set if you are using a subdomain to receive reports.


All other headers

Similarly, you can implement the report-uri parameter on any of the supported headers.

report-uri=\"https://report.example.com/report-uri/\"


5. You are done.

Restart Apache.

sudo service apache2 restart

You can test the reporting using cURL locally.

curl -i -H "Accept: application/json" -H "Content-Type: application/json" -X POST -d '{"json":{"data":"some data"}}' https://report.example.com/report-uri/



Comments are provided by Disqus. To respect user privacy, Disqus is only loaded on user prompt.

I recommend uBlock Origin to protect against Disqus tracking and advertising.