Skip to content
Snippets Groups Projects
CSRFProtection.php 5.31 KiB
Newer Older
<?php
# Lifter010: DONE

/**
 * CSRFProtection.php - protect from request forgery
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of
 * the License, or (at your option) any later version.
 *
 * @author      mlunzena@uos.de
 * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
 * @category    Stud.IP
 */

/**
 * To protect Stud.IP from forged request from other sites a security token is
 * generated and stored in the session and all forms (or rather POST request)
 * have to contain that token which is then compared on the server side to
 * verify the authenticity of the request. GET request are not checked as these
 * are assumed to be idempotent anyway.
 *
 * If a forgery is detected, an InvalidSecurityTokenException is thrown and a
 * log entry is recorded in the error log.
 *
 * The (form or request) parameter is named "security token". If you are
 * authoring an HTML form, you have to include this as an
 * input[@type=hidden] element. This is easily done by calling:
 *
 * \code
 * echo CSRFProtection::tokenTag();
 * \endcode
 *
 * Checking the token is implicitly done when calling #page_open in file
 * lib/phplib/page4.inc
 */
class CSRFProtection
{
    /**
     * The name of the parameter.
     */
    const TOKEN = 'security_token';

    const AJAX_TOKEN = 'HTTP_X_CSRF_TOKEN';

Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
    protected static $storage = null;

    /**
     * Set a storage to use.
     *
     * @param $storage
     */
    public static function setStorage(&$storage): void
    {
        self::$storage = &$storage;
    }

    /**
     * Returns a reference to the used storage.
     *
     * @return array|null
     */
    public static function &getStorage()
    {
        if (!isset(self::$storage)) {
            // w/o a session, throw an exception since we cannot use it
            if (session_id() === '') {
                throw new SessionRequiredException();
            }

Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
            self::$storage =& $_SESSION;
Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
        }
        return self::$storage;
    }

    /**
     * This checks the request and throws an InvalidSecurityTokenException if
     * fails to verify its authenticity.
     *
     * @throws MethodNotAllowedException      The request has to be unsafe
     *                                        in terms of RFC 2616.
     * @throws InvalidSecurityTokenException  The request is invalid as the
     *                                        security token does not match.
     */
    public static function verifyUnsafeRequest()
    {
        if (self::isSafeRequestMethod()) {
            throw new MethodNotAllowedException();
        }

        if (!self::checkSecurityToken()) {
            throw new InvalidSecurityTokenException();
        }
    }

    /**
     * @return boolean true if the request method is either GET or HEAD
     */
    private static function isSafeRequestMethod()
    {
        return in_array(Request::method(), ['GET', 'HEAD']);
    }

    /**
     * This checks the request and throws an InvalidSecurityTokenException if
     * fails to verify its authenticity.
     *
     * @throws InvalidSecurityTokenException  request is invalid
     */
    public static function verifySecurityToken()
    {
        if (!self::verifyRequest()) {
            throw new InvalidSecurityTokenException();
        }
    }

    /**
     * This checks the request and returns either true or false. It is
     * implicitly called by CSRFProtection::verifySecurityToken() and
     * it should never be needed to call this.
     *
     * @returns boolean  returns true if the request is valid
     */
    private static function verifyRequest()
    {
        return Request::isGet() || self::checkSecurityToken();
    }

    /**
     * Verifies the equality of the request parameter "security_token" and
     * the token stored in the session.
     *
     * @return boolean  true if equal
     */
    private static function checkSecurityToken()
    {
        return self::token() === ($_POST[self::TOKEN] ?? $_SERVER[self::AJAX_TOKEN] ?? null);
    }

    /**
     * Returns the token stored in the session generating it first
     * if required.
     *
     * @return string  a base64 encoded string of 32 random bytes
     * @throws SessionRequiredException  there is no session to store the token in
     */
    public static function token()
    {
Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
        $storage = &self::getStorage();

        // create a token, if there is none
Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
        if (!isset($storage[self::TOKEN])) {
            $storage[self::TOKEN] = base64_encode(random_bytes(32));
Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
        return $storage[self::TOKEN];
    }

    /**
     * Returns a snippet of HTML containing an input[@type=hidden] element
     * like this:
     *
     * \code
     * <input type="hidden" name="security_token" value="012345678901234567890123456789==">
     * \endcode
     *
     * @param array $attributes Additional attributes to be added to the input
     * @return string  the HTML snippet containing the input element
     */
    public static function tokenTag(array $attributes = [])
        $attributes = array_merge($attributes, [
            'name'  => self::TOKEN,
            'value' => self::token(),
        ]);

        return sprintf(
            '<input type="hidden" %s>',
            arrayToHtmlAttributes($attributes)