Make WordPress Core

Ticket #50510: secure-wp-nonces.3.php

File secure-wp-nonces.3.php, 4.8 KB (added by chaoix, 5 years ago)
Line 
1<?php
2/*
3Plugin Name: Secure WP Nonces
4Version: 1.1.0
5Author: Matthew Sigley
6Description: Reimplements the wp_create_nonce and wp_verify_nonce functions in a more secure way
7*/
8
9// Only override functions if none of them exist
10if( !function_exists( 'wp_create_nonce' ) && !function_exists( 'wp_verify_nonce' ) && !function_exists( 'wp_create_nonce_hash' ) ) {
11        // Adds a browser id to the nonce hash to add some variance to the hash output.
12        function wp_create_nonce( $action = -1 ) {
13                $user = wp_get_current_user();
14                $uid  = (int) $user->ID;
15                if ( ! $uid ) {
16                        /** This filter is documented in wp-includes/pluggable.php */
17                        $uid = apply_filters( 'nonce_user_logged_out', $uid, $action );
18                }
19
20                $token = '';
21                if( function_exists( 'wp_get_session_token' ) )
22                        $token = wp_get_session_token();
23                $i = wp_nonce_tick();
24
25                // Use UA string as part of the hash to help prevent token swaping
26                $browser_id = trim( (string) $_SERVER['HTTP_USER_AGENT'] );
27                $browser_id = hash( 'crc32b', $_SERVER['HTTP_USER_AGENT'] );
28
29                return wp_create_nonce_hash( $i, $action . '|' . $uid . '|' . $token . '|' . $browser_id );
30        }
31
32        // Amazon's S3 service uses a similar method for signing URLs.
33        // https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
34        // This allows plugin authors to add signing steps by providing a nonce action delimited by pipes |.
35        // For example "form-post|form-id|form-action" would result in 2 additional signing steps in addition to the 4 on every nonce hash.
36        function wp_create_nonce_hash( $nonce_tick, $action ) {
37                // Use sha256 for actions less then 56 bytes long, otherwise use sha512.
38                // In practice this means sha512 is used for nonces for logged in users.
39                // sha512 is faster than sha256 on 64bit platforms for longer strings.
40                // sha512 is available on php 5.1+.
41                // Intel provides a hardware version of sha256: https://en.wikipedia.org/wiki/Intel_SHA_extensions
42                // ARM provides a hardware version of sh256 and sha512: https://en.wikipedia.org/wiki/AArch64#AArch64_features https://en.wikipedia.org/wiki/AArch64#ARMv8.4-A
43                $hash_algo = 'sha256';
44                if( strlen( $action ) > 56 )
45                        $hash_algo = 'sha512';
46                $actions = explode( '|', $action );
47                $num_actions = count( $actions );
48                $salt = wp_salt( 'nonce' );
49                $nonce = hash_hmac( $hash_algo, $nonce_tick, "SWPN{$salt}", true ); // The salt prefix allows for existing hashes to be invalidated if a change is made to the algorithm.
50
51                for( $i = 1; $i < $num_actions; $i++ ) {
52                        $actions[$i] = (string) $actions[$i];
53                        if( $actions[$i] !== '' )
54                                $nonce = hash_hmac( $hash_algo, $actions[$i], $nonce, true );
55                }
56
57                // Truncate hash to 64 characters. This is the length of a sha256 hash.
58                $nonce = substr( hash_hmac( $hash_algo, $actions[0], $nonce ), 0, 64 );
59                return $nonce;
60        }
61
62        function wp_verify_nonce( $nonce, $action = -1 ) {
63                $nonce = (string) $nonce;
64                $user  = wp_get_current_user();
65                $uid   = (int) $user->ID;
66                if ( ! $uid ) {
67                        /**
68                         * Filters whether the user who generated the nonce is logged out.
69                         *
70                         * @since 3.5.0
71                         *
72                         * @param int    $uid    ID of the nonce-owning user.
73                         * @param string $action The nonce action.
74                         */
75                        $uid = apply_filters( 'nonce_user_logged_out', $uid, $action );
76                }
77
78                if ( '' === $nonce || strlen( $nonce ) !== 64 ) {
79                        return false;
80                }
81
82                $browser_id = trim( (string) $_SERVER['HTTP_USER_AGENT'] );
83                // Check for invalid UA strings
84                // We are validating only the <product>/<product-version> part of the UA string
85                // Format is documented here: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent
86                // This prevents lazy bots from submitting requests.
87                if( '' === $browser_id || !preg_match( '#^[^/]+/[\d\.]+#', $browser_id ) )
88                        return false;
89
90                $browser_id = hash( 'crc32b', $_SERVER['HTTP_USER_AGENT'] );
91
92                $token = '';
93                if( function_exists( 'wp_get_session_token' ) )
94                        $token = wp_get_session_token();
95                $i = wp_nonce_tick();
96
97                // Nonce generated 0-12 hours ago
98                $expected = wp_create_nonce_hash( $i, $action . '|' . $uid . '|' . $token . '|' . $browser_id );
99                if ( hash_equals( $expected, $nonce ) ) {
100                        return 1;
101                }
102
103                // Nonce generated 12-24 hours ago
104                $expected = wp_create_nonce_hash( ( $i - 1 ), $action . '|' . $uid . '|' . $token . '|' . $browser_id );
105                if ( hash_equals( $expected, $nonce ) ) {
106                        return 2;
107                }
108
109                /**
110                 * Fires when nonce verification fails.
111                 *
112                 * @since 4.4.0
113                 *
114                 * @param string     $nonce  The invalid nonce.
115                 * @param string|int $action The nonce action.
116                 * @param WP_User    $user   The current user object.
117                 * @param string     $token  The user's session token.
118                 */
119                do_action( 'wp_verify_nonce_failed', $nonce, $action, $user, $token, $browser_id );
120
121                // Invalid nonce
122                return false;
123        }
124}