PluginProbe ʕ •ᴥ•ʔ
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.12.6
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.12.6
2.12.6 2.12.5 2.12.4 2.12.3 2.12.2 2.12.1 2.12.0 2.11.1 2.11.0 2.10.1 2.10.0 2.9.1 2.9.0 2.8.2 2.8.1 2.7.0 2.7.1 2.8.0 trunk 0.0.10 0.0.11 0.0.12 0.0.13 0.0.2 0.0.3 0.0.4 0.0.5 0.0.6 0.0.7 0.0.8 0.0.9 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.1.0 1.1.1 1.1.2 1.10.0 1.10.1 1.11.0 1.12.0 1.12.1 1.12.2 1.12.3 1.13.0 1.13.1 1.13.2 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.3.0 1.3.1 1.3.2 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.5.0 1.5.1 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.8.0 1.9.0 1.9.1 2.0.0 2.0.1 2.0.2 2.1.0 2.1.1 2.2.0 2.2.1 2.2.2 2.3.0 2.4.0 2.5.0 2.5.2 2.6.0
sureforms / inc / submit-token.php
sureforms / inc Last commit date
abilities 3 days ago admin 3 months ago ai-form-builder 1 month ago blocks 2 months ago compatibility 1 month ago database 3 days ago email 1 month ago fields 1 month ago global-settings 3 days ago lib 1 month ago migrator 2 months ago page-builders 1 month ago payments 3 days ago single-form-settings 2 months ago traits 2 months ago activator.php 1 year ago admin-ajax.php 3 days ago background-process.php 9 months ago client-logger.php 3 days ago create-new-form.php 4 months ago duplicate-form.php 3 days ago entries.php 1 month ago events-scheduler.php 2 years ago export.php 3 days ago field-validation.php 1 month ago form-restriction.php 2 months ago form-styling.php 1 month ago form-submit.php 3 days ago form-views.php 3 days ago forms-data.php 3 days ago frontend-assets.php 3 days ago generate-form-markup.php 3 days ago gutenberg-hooks.php 2 weeks ago helper.php 3 days ago learn.php 4 months ago onboarding.php 2 months ago post-types.php 3 days ago rest-api.php 3 days ago smart-tags.php 1 week ago submit-token.php 3 days ago translatable.php 1 month ago updater-callbacks.php 1 month ago updater.php 1 month ago
submit-token.php
165 lines
1 <?php
2 /**
3 * Form Submission Token.
4 *
5 * Generates and verifies time-windowed HMAC tokens used to authenticate
6 * public form submissions. Unlike WordPress nonces (12–24 h expiry), these
7 * tokens are keyed on half-day windows and remain valid across multiple
8 * windows, making them safe to embed in cached pages without any client-side
9 * refresh logic.
10 *
11 * @package SureForms
12 * @since 2.6.0
13 */
14
15 namespace SRFM\Inc;
16
17 if ( ! defined( 'ABSPATH' ) ) {
18 exit;
19 }
20
21 /**
22 * Submit_Token
23 *
24 * Security properties:
25 * - HMAC-SHA256 keyed on the site's WordPress auth salt (unique per install).
26 * - Tokens are form-specific: a token issued for form 5 is invalid for form 6.
27 * - Each token encodes a half-day time window (43 200 s). Verification accepts
28 * the current window plus the previous SRFM_SUBMIT_TOKEN_ACCEPTED_WINDOWS − 1
29 * windows, giving up to 48 hours of validity. This covers even aggressive CDN
30 * or full-page-cache TTLs without any refresh round-trip from the browser.
31 * - Constant-time comparison (`hash_equals`) prevents timing-based oracle attacks.
32 *
33 * @since 2.6.0
34 */
35 class Submit_Token {
36 /**
37 * Length of a single time window in seconds (12 hours).
38 *
39 * Rotating every 12 hours keeps the attack window short while staying
40 * far below typical cache TTLs.
41 *
42 * @since 2.6.0
43 */
44 public const WINDOW_SECONDS = 43200;
45
46 /**
47 * Number of consecutive windows that are considered valid.
48 *
49 * 4 windows × 12 hours = 48 hours maximum token lifetime.
50 * Raise via the `srfm_submit_token_accepted_windows` filter if your
51 * deployment uses unusually long cache TTLs.
52 *
53 * @since 2.6.0
54 */
55 public const DEFAULT_ACCEPTED_WINDOWS = 4;
56
57 /**
58 * Namespace for tokens that authorise a form submission.
59 *
60 * The value is the historical payload prefix, so existing tokens keep verifying
61 * across an upgrade — changing it would reject every token already embedded in
62 * cached HTML.
63 *
64 * @since 2.12.6
65 */
66 public const NAMESPACE_SUBMIT = 'srfm_submit';
67
68 /**
69 * Namespace for tokens that authorise the page-view beacon.
70 *
71 * Separate from NAMESPACE_SUBMIT so the two cannot stand in for each other: a
72 * view token scraped from the page must not authorise a submission, and the
73 * view endpoint must not double as an oracle for whether a submit token is
74 * still inside an accepted window.
75 *
76 * @since 2.12.6
77 */
78 public const NAMESPACE_VIEW = 'srfm_view';
79
80 /**
81 * Generate a submission token for a given form.
82 *
83 * The token encodes the form ID and the current half-day window, signed
84 * with the site's auth salt. It is safe to embed in cached HTML because
85 * `verify()` accepts several consecutive past windows.
86 *
87 * @since 2.6.0
88 * @since 2.12.6 Added the $namespace parameter.
89 * @param int $form_id The form post ID.
90 * @param string $namespace Purpose the token is minted for. Defaults to form submission.
91 * @return string 64-character lowercase hex HMAC-SHA256 token.
92 */
93 public static function generate( int $form_id, string $namespace = self::NAMESPACE_SUBMIT ): string {
94 return self::sign( $form_id, self::current_window(), $namespace );
95 }
96
97 /**
98 * Verify a token submitted with a form.
99 *
100 * Checks the token against every accepted window, from newest to oldest,
101 * using constant-time comparison throughout.
102 *
103 * @since 2.6.0
104 * @since 2.12.6 Added the $namespace parameter.
105 * @param string $token Token value received from the client.
106 * @param int $form_id Form post ID extracted from the request body.
107 * @param string $namespace Purpose the token must have been minted for.
108 * @return bool True if the token is valid for the given form, false otherwise.
109 */
110 public static function verify( string $token, int $form_id, string $namespace = self::NAMESPACE_SUBMIT ): bool {
111 if ( '' === $token || $form_id <= 0 ) {
112 return false;
113 }
114
115 // Clamp to [1, 14]: must accept at least one window; cap at 14 (7 days)
116 // to prevent a misconfigured filter from making tokens effectively permanent.
117 $accepted = max( 1, min( 14, (int) apply_filters( 'srfm_submit_token_accepted_windows', self::DEFAULT_ACCEPTED_WINDOWS ) ) );
118
119 // Walk backwards through accepted windows; current window first.
120 for ( $offset = 0; $offset < $accepted; $offset++ ) {
121 if ( hash_equals( self::sign( $form_id, self::current_window() - $offset, $namespace ), $token ) ) {
122 return true;
123 }
124 }
125
126 return false;
127 }
128
129 /**
130 * Return the index of the current half-day window.
131 *
132 * Dividing Unix time by WINDOW_SECONDS yields an integer that increments
133 * once every 12 hours, regardless of the server's local timezone.
134 *
135 * @since 2.6.0
136 * @return int
137 */
138 private static function current_window(): int {
139 return (int) floor( time() / self::WINDOW_SECONDS );
140 }
141
142 /**
143 * Build an HMAC-SHA256 signature for a specific form / window pair.
144 *
145 * The payload deliberately combines a namespace prefix, the form ID, and
146 * the window index so that tokens cannot be repurposed across forms or
147 * replayed across time windows.
148 *
149 * @since 2.6.0
150 * @param int $form_id Post ID of the form.
151 * @param int $window Half-day window index.
152 * @param string $namespace Purpose prefix; keeps tokens for one action from authorising another.
153 * @return string 64-character lowercase hex digest.
154 */
155 private static function sign( int $form_id, int $window, string $namespace = self::NAMESPACE_SUBMIT ): string {
156 // Derive a plugin-specific sub-key from the site's auth salt so this
157 // system has an independent key surface from WordPress session cookies.
158 // Rotating wp-config.php secrets invalidates all outstanding tokens, which
159 // is intentional — a cache purge should follow any secret key rotation.
160 $signing_key = hash_hmac( 'sha256', 'srfm-submit-token-v1', wp_salt( 'auth' ) );
161 $payload = implode( '|', [ $namespace, $form_id, $window ] );
162 return hash_hmac( 'sha256', $payload, $signing_key );
163 }
164 }
165