PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / trunk
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! vtrunk
3.8.0 3.7.5 3.7.4 3.7.3 3.7.2 1-final 3.7.1 3.7.0 3.6.8 3.6.7 3.6.6 3.6.5 3.6.4 3.6.3 3.6.2 3.6.1 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 All 112 releases
templately / modules / full-site-import / Utils / SignatureVerifier.php

SignatureVerifier.php in Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! trunk, at modules/full-site-import/Utils/SignatureVerifier.php

201 lines 7.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Templately\Modules\FullSiteImport\Utils;
3
4 use Templately\Utils\Helper;
5 use WP_REST_Request;
6
7 /**
8 * Class SignatureVerifier
9 *
10 * Verifies HMAC-SHA256 signatures from Templately backend callbacks
11 * to prevent arbitrary file write vulnerabilities.
12 *
13 * @package Templately\Modules\FullSiteImport\Utils
14 */
15 class SignatureVerifier {
16
17 /**
18 * Verify callback signature from Templately backend.
19 *
20 * @param array $payload Request payload data
21 * @param string $signature Signature from X-Templately-Signature header
22 * @param int $timestamp Timestamp from X-Templately-Timestamp header
23 * @param string $api_key User's API key (used as secret)
24 * @param int $tolerance Time tolerance in seconds (default: 300 = 5 minutes)
25 * @return bool|WP_Error True if valid, WP_Error otherwise
26 */
27 public static function verify($payload, $signature, $timestamp, $api_key, $tolerance = 300) {
28 if (empty($api_key)) {
29 return Helper::error(
30 'missing_api_key',
31 __('API key not provided for signature verification', 'templately'),
32 'verify_signature',
33 401
34 );
35 }
36
37 // Validate inputs
38 if (empty($signature) || empty($timestamp) || !is_numeric($timestamp)) {
39 return Helper::error(
40 'invalid_signature_headers',
41 __('Invalid signature or timestamp headers', 'templately'),
42 'verify_signature',
43 401
44 );
45 }
46
47 // Check timestamp to prevent replay attacks
48 if (!self::is_timestamp_valid((int) $timestamp, $tolerance)) {
49 return Helper::error(
50 'timestamp_expired',
51 __('Callback timestamp expired or invalid', 'templately'),
52 'verify_signature',
53 401
54 );
55 }
56
57 // Generate expected signature using API key
58 $expected_signature = self::generate_signature($payload, (int) $timestamp, $api_key);
59
60 // Use hash_equals to prevent timing attacks
61 if (!hash_equals($expected_signature, $signature)) {
62 return Helper::error(
63 'invalid_signature',
64 __('Invalid signature', 'templately'),
65 'verify_signature',
66 401
67 );
68 }
69
70 return true;
71 }
72
73 /**
74 * Whether callback signature verification REJECTS (enforce) or only LOGS (log-only).
75 *
76 * Defaults to log-only (034 FR-001): during the rollout grace window a mismatch is
77 * logged but the request is allowed, so legitimate cloud traffic can never be rejected
78 * before signed callbacks are confirmed. Flip to enforce by defining the
79 * TEMPLATELY_CALLBACK_SIGNATURE_ENFORCE constant true, or via the
80 * `templately_callback_signature_enforce` filter, once signed traffic is confirmed.
81 * The flag is time-boxed — to be removed when enforce becomes the permanent default.
82 *
83 * @return bool True when a verification failure must reject the request.
84 */
85 public static function is_enforced() {
86 if ( defined( 'TEMPLATELY_CALLBACK_SIGNATURE_ENFORCE' ) ) {
87 return (bool) TEMPLATELY_CALLBACK_SIGNATURE_ENFORCE;
88 }
89
90 return (bool) apply_filters( 'templately_callback_signature_enforce', false );
91 }
92
93 /**
94 * Verify a callback request and apply the rollout mode (034 FR-001).
95 *
96 * Reads the signature + timestamp headers, verifies them against the canonical
97 * payload, and decides whether the request may proceed:
98 * - on a valid signature → true;
99 * - on a failure in enforce mode → the WP_Error from verify() (caller returns it → 401);
100 * - on a failure in log-only mode → logs the failure code and returns true (allowed).
101 *
102 * @param WP_REST_Request $request The callback request.
103 * @param string $api_key The already-validated API key (HMAC secret).
104 * @param string $context Endpoint/log context tag.
105 * @return true|\WP_Error
106 */
107 public static function verify_request( WP_REST_Request $request, $api_key, $context = 'callback' ) {
108 $signature = $request->get_header( 'x_templately_signature' );
109 if ( empty( $signature ) ) {
110 $signature = $request->get_header( 'X-Templately-Signature' );
111 }
112
113 $timestamp = $request->get_header( 'x_templately_timestamp' );
114 if ( empty( $timestamp ) ) {
115 $timestamp = $request->get_header( 'X-Templately-Timestamp' );
116 }
117
118 $result = self::verify( $request->get_params(), $signature, $timestamp, $api_key );
119 if ( true === $result ) {
120 return true;
121 }
122
123 // verify() returned a WP_Error.
124 if ( self::is_enforced() ) {
125 return $result;
126 }
127
128 // Log-only grace window: record the failure, allow the request through.
129 Helper::log( [
130 'context' => $context,
131 'code' => is_wp_error( $result ) ? $result->get_error_code() : 'unknown',
132 'message' => is_wp_error( $result ) ? $result->get_error_message() : '',
133 ], 'callback_signature_unverified' );
134
135 return true;
136 }
137
138 /**
139 * Check if timestamp is within acceptable tolerance.
140 *
141 * @param int $timestamp Unix timestamp to check
142 * @param int $tolerance Tolerance in seconds
143 * @return bool True if timestamp is valid
144 */
145 private static function is_timestamp_valid($timestamp, $tolerance) {
146 $current_time = time();
147 $time_difference = abs($current_time - $timestamp);
148
149 return $time_difference <= $tolerance;
150 }
151
152 /**
153 * Generate HMAC-SHA256 signature for payload.
154 *
155 * @param array $payload Request payload
156 * @param int $timestamp Unix timestamp
157 * @param string $api_key User's API key (used as secret)
158 * @return string HMAC signature
159 */
160 private static function generate_signature($payload, $timestamp, $api_key) {
161 $canonical_string = self::create_canonical_string($payload, $timestamp);
162 return hash_hmac('sha256', $canonical_string, $api_key);
163 }
164
165 /**
166 * Create canonical string from payload and timestamp.
167 *
168 * Only includes security-critical fields in signature to avoid
169 * performance issues with large template content.
170 *
171 * @param array $payload Request payload
172 * @param int $timestamp Unix timestamp
173 * @return string Canonical string
174 */
175 private static function create_canonical_string($payload, $timestamp) {
176 // Extract only security-critical fields for signature
177 // Exclude large content fields like 'template' and 'error'
178 // Also exclude 'isSkipped' as requested
179 $signature_fields = [
180 'process_id' => isset($payload['process_id']) ? $payload['process_id'] : null,
181 'content_id' => isset($payload['content_id']) ? $payload['content_id'] : null,
182 'template_id' => isset($payload['template_id']) ? $payload['template_id'] : null,
183 'type' => isset($payload['type']) ? $payload['type'] : null,
184 ];
185
186 // Remove null values
187 $signature_fields = array_filter($signature_fields, function ($value) {
188 return $value !== null;
189 });
190
191 // Sort keys for consistency
192 ksort($signature_fields);
193
194 // JSON encode with consistent flags
195 $payload_json = wp_json_encode($signature_fields, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
196
197 // Combine timestamp and payload
198 return $timestamp . '.' . $payload_json;
199 }
200 }
201