How to code a website security audit using PHP and WordPress

on in WordPress
Last modified on

Website Speed Audit/Test

This tutorial will teach you how to build a website security audit in the form of a WordPress plugin.

We will use cURL and Mozilla’s Observatory feature.

The audit tests the website headers, server software (if available), Cloudflare, SSL, TLS and more.

Let’s start with an HTML form:

<form class="whiskey-form whiskey-form--security" method="get" action="/security-audit/">
    <p>
        <label for="whiskey-url">Your Website URL:</label><br>
        <input type="url" name="whiskey_url" id="whiskey-url" placeholder="https://">
        <br><small>e.g. <code>https://getbutterfly.com/</code></small>
    </p>
    <p>
        <input type="submit" name="whiskey_go" id="whiskey-url--go" value="Run Security Audit">
    </p>
</form>

As this is a WordPress plugin, this form goes into a PHP function. The entire function is wrapped as a shortcode. Here is the entire code:

/**
 * Generate security grade
 *
 * @param  string $url Website URL to be checked
 * @return array       Array of present headers
 */
function sf_get_security_score( $url ) {
    $url_headers       = get_headers( $url, true );
    $url_headers_array = [];

    if ( ! empty( $url_headers ) ) {
        $status = substr( $url_headers[0], 9 );
        if ( (int) $status === 200 ) {
            $url_headers_array[] = 'SSL';
            $url_headers_array[] = 'TLS 1.2+';
        }

        $security_headers = [
            'X-Content-Type-Options',
            'X-Frame-Options',
            'Content-Security-Policy',
            'Strict-Transport-Security',
            'Referrer-Policy',
            'Permissions-Policy',
            'Expect-CT',
        ];

        foreach ( $url_headers as $key => $value ) {
            if ( in_array( (string) $key, $security_headers ) ) {
                $url_headers_array[] = (string) $key;
            }

            if ( (string) $key === 'X-Powered-By' ) {
                // Bad
                $url_headers_array[] = 'X-Powered-By';
            }
            if ( (string) $key === 'Server' && (string) $value !== 'cloudflare' ) {
                // Bad
                $url_headers_array[] = 'Server';
            }
            if ( (string) $key === 'CF-RAY' ) {
                // Good
                $url_headers_array[] = 'Cloudflare';
            }
        }
    }

    return $url_headers_array;
}



/**
 * Get security grade
 *
 * Gets security grade based on a number of factors.
 *
 * @param  int $id Audit ID
 * @return int $grade
 */
function sf_get_security_grade( $security_headers ) {
    if ( empty( $security_headers ) ) {
        return 0;
    }
    $security_headers_array = explode( ',', $security_headers );
    $security_headers_count = count( $security_headers_array );

    return get_score_percentage( 10, (int) $security_headers_count );
}

function get_score_percentage( $total, $number ) {
    return ( (int) $total > 0 ) ? round( $number / ( $total / 100 ) ) : 0;
}



function whiskey_security_audit() {
    $security_headers_checklist = [
        'SSL',
        'TLS 1.2+',
        'X-Content-Type-Options',
        'X-Frame-Options',
        'Content-Security-Policy',
        'Strict-Transport-Security',
        'Referrer-Policy',
        'Permissions-Policy',
        'Expect-CT',
        'Cloudflare',
    ];

    // Only these headers count for the grade
    $security_headers_audit_checklist = [
        'SSL',
        'TLS 1.2+',
        'X-Content-Type-Options',
        'X-Frame-Options',
        'Content-Security-Policy',
        'Strict-Transport-Security',
        'Referrer-Policy',
        'Permissions-Policy',
    ];

    $out = '<form class="whiskey-form whiskey-form--security" method="get" action="https://getbutterfly.com/tools/security-audit/">
        <p>
            <label for="whiskey-url">Your Website URL:</label><br>
            <input type="url" name="whiskey_url" id="whiskey-url" placeholder="https://">
            <br><small>e.g. <code>https://getbutterfly.com/</code></small>
        </p>
        <p>
            <input type="submit" name="whiskey_go" id="whiskey-url--go" value="Run Security Audit">
        </p>
    </form>';

    if ( isset( $_GET['whiskey_go'] ) ) {
        $whiskey_url = esc_url_raw( $_GET['whiskey_url'] );

        if ( (string) $whiskey_url !== '' ) {
            $audit_security_array   = sf_get_security_score( $whiskey_url );
            $audit_security_headers = implode( ',', $audit_security_array );

            $score = sf_get_security_grade( $audit_security_headers );

            $security_headers = $audit_security_headers;

            $out .= '<div class="wp-block-columns">
                <div class="wp-block-column">
                    <h3>Security Headers</h3>';

                    if ( ! empty( $security_headers ) ) {
                        $security_headers_array = explode( ',', $security_headers );
                        $security_headers_count = count( (array) $security_headers_array );
                        $url_grade              = 'g';
                        $url_grade_count        = 0;

                        foreach ( $security_headers_audit_checklist as $header_grade ) {
                            $url_grade_count = ( in_array( $header_grade, $security_headers_array ) ) ? ( $url_grade_count + 1 ) : $url_grade_count;
                        }

                        if ( (int) $url_grade_count > 8 ) {
                            $url_grade = 'a+';
                        } else {
                            $grades    = [ 'g', 'g', 'f', 'e', 'd', 'c', 'b', 'a', 'a+' ];
                            $url_grade = $grades[ (int) $url_grade_count ];
                        }

                        $out .= '<div class="sf-grade sf-grade--' . str_replace( '+', '', $url_grade ) . '">' . $url_grade . '</div>
                        <div class="sf-grade sf-grade--' . str_replace( '+', '', $url_grade ) . '" style="width: 160px;">' . $score . '<small>/100</small></div>
                        <div class="sf-grade-headers">';
                            foreach ( $security_headers_checklist as $security_header ) {
                                if ( in_array( $security_header, $security_headers_array ) ) {
                                    $out .= '<div class="sf-grade-header sf-grade-header--enabled">' . $security_header . '</div>';
                                } else {
                                    $out .= '<div class="sf-grade-header sf-grade-header--disabled">' . $security_header . '</div>';
                                }
                            }
                        $out .= '</div>';

                        if ( stripos( $security_headers, 'Server' ) !== false && stripos( $security_headers, 'Cloudflare' ) === false ) {
                            $out .= '<p class="sf-grade--negative">
                                Note that your server technology is exposed.
                                <br><small>You should remove the <code>Server</code> header. <a href="https://getbutterfly.com/security-headers-a-concise-guide/#x-powered-by">How?</a></small>
                            </p>';
                        }
                        if ( stripos( $security_headers, 'X-Powered-By' ) !== false ) {
                            $out .= '<p class="sf-grade--negative">
                                Note that your server software is exposed.
                                <br><small>You should remove the <code>X-Powered-By</code> header. <a href="https://getbutterfly.com/security-headers-a-concise-guide/#x-powered-by">How?</a></small>
                            </p>';
                        }
                        if ( stripos( $security_headers, 'Cloudflare' ) !== false ) {
                            $out .= '<p class="sf-grade--positive">
                                You are using Cloudflare.
                                <br><small>Cloudflare helps with security and site speed.</small>
                            </p>';
                        }

                    } else {
                        $out .= '<p>No data.</p>';
                    }
                $out .= '</div>
                <div class="wp-block-column">
                    <h3>Tips</h3>
                    <ul>
                        <li>Most security vulnerabilities can be fixed by <a href="https://getbutterfly.com/security-headers-a-concise-guide/">implementing the necessary headers</a> in the HTTP response.</li>
                        <li>We use headers and connection flags to calculate the security score.</li>
                        <li>TLS encryption can help protect web applications from attacks such as data breaches, and DDoS attacks. Additionally, TLS-protected HTTPS is quickly becoming a standard practice for websites. The most recent version is TLS 1.3, which was published in 2018.</li>
                    </ul>
                </div>
            </div>';

            // Raw headers
            $server_note = '<br><small style="color: #e74c3c;">Trying to minimise the amount of information you give out about your server is a good idea. This header should be removed or the value changed.</small>';

            $headers = get_headers( $whiskey_url, 1 );
            asort( $headers );

            $out .= '<h3>Raw Headers</h3>';

            foreach ( $headers as $key => $value ) {
                if ( is_array( $value ) ) {
                    $value = array_map( 'htmlentities', array_filter( array_unique( $value ) ) );
                    $value = implode( '<br>', $value );
                }

                // Good
                $key = preg_replace(
                    [
                        '/Strict-Transport-Security/i',
                        '/X-Frame-Options/i',
                        '/X-Content-Type-Options/i',
                        '/Referrer-Policy/i',
                        '/Permissions-Policy/i',
                        '/Content-Security-Policy/i',
                    ],
                    [
                        '<b style="color: #1abc9c;">Strict-Transport-Security</b>',
                        '<b style="color: #1abc9c;">X-Frame-Options</b>',
                        '<b style="color: #1abc9c;">X-Content-Type-Options</b>',
                        '<b style="color: #1abc9c;">Referrer-Policy</b>',
                        '<b style="color: #1abc9c;">Referrer-Policy</b>',
                        '<b style="color: #1abc9c;">Permissions-Policy</b>',
                        '<b style="color: #1abc9c;">Content-Security-Policy</b>',
                    ],
                    $key
                );

                // Bad
                $key = preg_replace(
                    [
                        '/Server/i',
                        '/X-Powered-By/i',
                    ],
                    [
                        '<b style="color: #e74c3c;">Server</b>',
                        '<b style="color: #e74c3c;">X-Powered-By</b>',
                    ],
                    $key
                );

                if ( strpos( $key, 'Server' ) !== false || strpos( $key, 'X-Powered-By' ) !== false ) {
                    $value .= $server_note;
                }

                $out .= '<div class="wp-block-columns wp-block-columns--grade-headers">
                    <div class="wp-block-column" style="flex-basis:33.33%">
                        <code>' . $key . '</code>
                    </div>
                    <div class="wp-block-column" style="flex-basis:66.66%">' . $value . '</div>
                </div>';
            }

            // Mozilla Observatory
            $curl = curl_init();

            curl_setopt_array(
                $curl,
                [
                    CURLOPT_URL            => 'https://http-observatory.security.mozilla.org/api/v1/analyze?host=' . parse_url( $whiskey_url, PHP_URL_HOST ) . '&rescan=true',
                    CURLOPT_RETURNTRANSFER => true,
                    CURLOPT_ENCODING       => '',
                    CURLOPT_MAXREDIRS      => 10,
                    CURLOPT_TIMEOUT        => 30,
                    CURLOPT_CUSTOMREQUEST  => 'POST',
                    CURLOPT_HTTPHEADER     => [
                        'Accept: */*',
                        'Accept-Encoding: gzip, deflate',
                        'Cache-Control: no-cache',
                        'Connection: keep-alive',
                        'Content-Length: 0',
                        'Host: http-observatory.security.mozilla.org',
                        'User-Agent: PostmanRuntime/7.18.0',
                        'cache-control: no-cache',
                    ],
                ]
            );

            $response = curl_exec( $curl );
            $err      = curl_error( $curl );

            curl_close( $curl );

            $out .= '<h3>Mozilla Observatory</h3>';

            if ( $err ) {
                $out .= 'An error has occurred.';
            } else {
                $response = (array) $response;
                $response = json_decode( $response[0], true );

                $out .= '<p><img src="https://getbutterfly.com/wp-content/plugins/whiskey/mozilla-logo-bw-rgb.png" alt="Mozilla Observatory" loading="lazy" width="84" height="24"></p>
                <p>Observatory audit <code>' . $response['scan_id'] . '</code> initiated.<br><small>If no result is available, try again in 3 to 5 minutes. The Observatory allows you to scan your site every five minutes.</small></p>

                <div style="height:24px" aria-hidden="true" class="wp-block-spacer"></div>';

                $curl = curl_init();

                curl_setopt_array(
                    $curl,
                    [
                        CURLOPT_URL            => 'https://http-observatory.security.mozilla.org/api/v1/getScanResults?scan=' . $response['scan_id'],
                        CURLOPT_RETURNTRANSFER => true,
                        CURLOPT_ENCODING       => '',
                        CURLOPT_MAXREDIRS      => 10,
                        CURLOPT_TIMEOUT        => 30,
                        CURLOPT_CUSTOMREQUEST  => 'GET',
                        CURLOPT_HTTPHEADER     => [
                            'cache-control: no-cache',
                        ],
                    ]
                );

                $response = curl_exec( $curl );
                $err      = curl_error( $curl );

                curl_close( $curl );

                if ( $err ) {
                    $out .= 'An error has occurred.';
                } else {
                    $response = (array) $response;
                    $response = json_decode( $response[0], true );

                    $score_modifier = 100;

                    // Help array
                    $score_help_array = [
                        'content-security-policy'       => 'Content Security Policy (CSP) can prevent a wide range of cross-site scripting (XSS) and clickjacking attacks against your website.',
                        'cookies'                       => 'Using cookies attributes such as Secure and HttpOnly can protect users from having their personal information stolen.',
                        'cross-origin-resource-sharing' => 'Incorrectly configured CORS settings can allow foreign sites to read your site\'s contents, possibly allowing them access to private user information.',
                        'public-key-pinning'            => 'HTTP Public Key Pinning (HPKP) binds a site to a specific combination of certificate authorities and/or keys, protecting against the unauthorized issuance of certificates.',
                        'redirection'                   => 'Properly configured redirections from HTTP to HTTPS allow browsers to correctly apply HTTP Strict Transport Security (HSTS) settings.',
                        'referrer-policy'               => 'Referrer Policy can protect the privacy of your users by restricting the contents of the HTTP Referer header.',
                        'strict-transport-security'     => 'Properly configured redirections from HTTP to HTTPS allow browsers to correctly apply HTTP Strict Transport Security (HSTS) settings.',
                        'subresource-integrity'         => 'Subresource Integrity protects against JavaScript files and stylesheets stored on content delivery networks (CDNs) from being maliciously modified.',
                        'x-content-type-options'        => 'X-Content-Type-Options instructs browsers to not guess the MIME types of files that the web server is delivering.',
                        'x-frame-options'               => 'X-Frame-Options controls whether your site can be framed, protecting against clickjacking attacks. It has been superseded by Content Security Policy\'s <code>frame-ancestors</code> directive, but should still be used for now.',
                        'x-xss-protection'              => 'X-XSS-Protection protects against reflected cross-site scripting (XSS) attacks in IE and Chrome, but has been superseded by Content Security Policy. It can still be used to protect users of older web browsers.',
                    ];

                    foreach ( $response as $test ) {
                        $score_modifier += (int) $test['score_modifier'];

                        $out .= '<div class="wp-block-columns wp-block-columns--mozilla-observatory">
                            <div class="wp-block-column" style="flex-basis:30%"><code>' . $test['name'] . '</code></div>
                            <div class="wp-block-column" style="flex-basis:5%">' . ( (int) $test['pass'] === 1 ? '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="svg-inline--fa fa-check fa-w-14" data-icon="check" data-prefix="fal" viewBox="0 0 448 512"><defs/><path fill="#1abc9c" d="M413.5 92l-280 280-99-99a12 12 0 00-17 0L6.2 284.3a12 12 0 000 17L125 420a12 12 0 0017 0l299.8-299.8a12 12 0 000-17L430.5 92a12 12 0 00-17 0z"/></svg>' : '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="svg-inline--fa fa-times fa-w-10" data-icon="times" data-prefix="fal" viewBox="0 0 320 512"><defs/><path fill="#e74c3c" d="M194 256l102.5-102.6 21.1-21.1a8 8 0 000-11.3L295 98.3a8 8 0 00-11.3 0L160 222.1 36.3 98.3a8 8 0 00-11.3 0L2.3 121a8 8 0 000 11.3L126.1 256 2.3 379.7a8 8 0 000 11.3L25 413.6a8 8 0 0011.3 0L160 290l102.6 102.6 21.1 21.1a8 8 0 0011.3 0l22.6-22.6a8 8 0 000-11.3L194 256z"/></svg>' ) . '</div>
                            <div class="wp-block-column" style="flex-basis:5%">' . $test['score_modifier'] . '</div>
                            <div class="wp-block-column" style="flex-basis:60%">
                                <p>' .
                                    $test['score_description'] .
                                    '<br><small>' . $score_help_array[ $test['name'] ] . '</small>
                                </p>
                                <div style="height:24px" aria-hidden="true" class="wp-block-spacer"></div>
                            </div>
                        </div>';
                    }

                    $out .= '<p>Observatory audit score is <b>' . $score_modifier . '</b>/100.</p>
                    <div style="height:24px" aria-hidden="true" class="wp-block-spacer"></div>';
                }
            }
        }
    }

    return $out;
}

add_shortcode( 'audit', 'whiskey_security_audit' );

Use it by adding the [audit] shortcode on any post or page, or by directly using the whiskey_security_audit() function in any of your page templates.

Related posts