1818use CodeIgniter \Exceptions \LogicException ;
1919use CodeIgniter \HTTP \IncomingRequest ;
2020use CodeIgniter \HTTP \Method ;
21- use CodeIgniter \HTTP \Request ;
2221use CodeIgniter \HTTP \RequestInterface ;
2322use CodeIgniter \I18n \Time ;
2423use CodeIgniter \Security \Exceptions \SecurityException ;
2524use CodeIgniter \Session \Session ;
2625use Config \Cookie as CookieConfig ;
2726use Config \Security as SecurityConfig ;
2827use ErrorException ;
28+ use JsonException ;
2929use SensitiveParameter ;
3030
3131/**
@@ -233,32 +233,27 @@ private function configureCookie(CookieConfig $cookie): void
233233 Cookie::setDefaults ($ cookie );
234234 }
235235
236- /**
237- * CSRF verification.
238- *
239- * @return $this
240- *
241- * @throws SecurityException
242- */
243236 public function verify (RequestInterface $ request )
244237 {
245- // Protects POST, PUT, DELETE, PATCH
246- $ method = $ request -> getMethod ();
247- $ methodsToProtect = [Method:: POST , Method:: PUT , Method:: DELETE , Method:: PATCH ];
248- if (! in_array ($ method , $ methodsToProtect , true )) {
238+ $ method = $ request -> getMethod ();
239+
240+ // Protect POST, PUT, DELETE, PATCH requests only
241+ if (! in_array ($ method , [Method:: POST , Method:: PUT , Method:: DELETE , Method:: PATCH ] , true )) {
249242 return $ this ;
250243 }
251244
245+ assert ($ request instanceof IncomingRequest);
246+
252247 $ postedToken = $ this ->getPostedToken ($ request );
253248
254249 try {
255- $ token = ($ postedToken !== null && $ this ->config ->tokenRandomize )
256- ? $ this ->derandomize ($ postedToken ) : $ postedToken ;
250+ $ token = $ postedToken !== null && $ this ->config ->tokenRandomize
251+ ? $ this ->derandomize ($ postedToken )
252+ : $ postedToken ;
257253 } catch (InvalidArgumentException ) {
258254 $ token = null ;
259255 }
260256
261- // Do the tokens match?
262257 if (! isset ($ token , $ this ->hash ) || ! hash_equals ($ this ->hash , $ token )) {
263258 throw SecurityException::forDisallowedAction ();
264259 }
@@ -277,66 +272,107 @@ public function verify(RequestInterface $request)
277272 /**
278273 * Remove token in POST or JSON request data
279274 */
280- private function removeTokenInRequest (RequestInterface $ request ): void
275+ private function removeTokenInRequest (IncomingRequest $ request ): void
281276 {
282- assert ($ request instanceof Request);
283-
284277 $ superglobals = service ('superglobals ' );
285- if ($ superglobals ->post ($ this ->config ->tokenName ) !== null ) {
286- // We kill this since we're done and we don't want to pollute the POST array.
287- $ superglobals ->unsetPost ($ this ->config ->tokenName );
278+ $ tokenName = $ this ->config ->tokenName ;
279+
280+ // If the token is found in POST data, we can safely remove it.
281+ if (is_string ($ superglobals ->post ($ tokenName ))) {
282+ $ superglobals ->unsetPost ($ tokenName );
288283 $ request ->setGlobal ('post ' , $ superglobals ->getPostArray ());
289- } else {
290- $ body = $ request ->getBody () ?? '' ;
291- $ json = json_decode ($ body );
292- if ($ json !== null && json_last_error () === JSON_ERROR_NONE ) {
293- // We kill this since we're done and we don't want to pollute the JSON data.
294- unset($ json ->{$ this ->config ->tokenName });
295- $ request ->setBody (json_encode ($ json ));
296- } else {
297- parse_str ($ body , $ parsed );
298- // We kill this since we're done and we don't want to pollute the BODY data.
299- unset($ parsed [$ this ->config ->tokenName ]);
300- $ request ->setBody (http_build_query ($ parsed ));
301- }
284+
285+ return ;
286+ }
287+
288+ $ body = $ request ->getBody () ?? '' ;
289+
290+ if ($ body === '' ) {
291+ return ;
292+ }
293+
294+ // If the token is found in JSON data, we can safely remove it.
295+ try {
296+ $ json = json_decode ($ body , flags: JSON_THROW_ON_ERROR );
297+ } catch (JsonException ) {
298+ $ json = null ;
302299 }
300+
301+ if (is_object ($ json ) && property_exists ($ json , $ tokenName )) {
302+ unset($ json ->{$ tokenName });
303+ $ request ->setBody (json_encode ($ json ));
304+
305+ return ;
306+ }
307+
308+ // If the token is found in form-encoded data, we can safely remove it.
309+ parse_str ($ body , $ result );
310+ unset($ result [$ tokenName ]);
311+ $ request ->setBody (http_build_query ($ result ));
303312 }
304313
305- private function getPostedToken (RequestInterface $ request ): ?string
314+ private function getPostedToken (IncomingRequest $ request ): ?string
306315 {
307- assert ($ request instanceof IncomingRequest);
316+ $ tokenName = $ this ->config ->tokenName ;
317+ $ headerName = $ this ->config ->headerName ;
308318
309- // Does the token exist in POST, HEADER or optionally php:://input - json data or PUT, DELETE, PATCH - raw data.
319+ // 1. Check POST data first.
320+ $ token = $ request ->getPost ($ tokenName );
310321
311- if ($ tokenValue = $ request -> getPost ( $ this -> config -> tokenName )) {
312- return is_string ( $ tokenValue ) ? $ tokenValue : null ;
322+ if ($ this -> isNonEmptyTokenString ( $ token )) {
323+ return $ token ;
313324 }
314325
315- if ($ request ->hasHeader ($ this ->config ->headerName )) {
316- $ tokenValue = $ request ->header ($ this ->config ->headerName )->getValue ();
326+ // 2. Check header data next.
327+ if ($ request ->hasHeader ($ headerName )) {
328+ $ token = $ request ->header ($ headerName )->getValue ();
317329
318- return (is_string ($ tokenValue ) && $ tokenValue !== '' ) ? $ tokenValue : null ;
330+ if ($ this ->isNonEmptyTokenString ($ token )) {
331+ return $ token ;
332+ }
319333 }
320334
321- $ body = (string ) $ request ->getBody ();
335+ // 3. Finally, check the raw input data for JSON or form-encoded data.
336+ $ body = $ request ->getBody () ?? '' ;
322337
323- if ($ body !== '' ) {
324- $ json = json_decode ($ body );
325- if ($ json !== null && json_last_error () === JSON_ERROR_NONE ) {
326- $ tokenValue = $ json ->{$ this ->config ->tokenName } ?? null ;
338+ if ($ body === '' ) {
339+ return null ;
340+ }
327341
328- return is_string ($ tokenValue ) ? $ tokenValue : null ;
342+ // 3a. Check if a JSON payload exists and contains the token.
343+ try {
344+ $ json = json_decode ($ body , flags: JSON_THROW_ON_ERROR );
345+ } catch (JsonException ) {
346+ $ json = null ;
347+ }
348+
349+ if (is_object ($ json ) && property_exists ($ json , $ tokenName )) {
350+ $ token = $ json ->{$ tokenName };
351+
352+ if ($ this ->isNonEmptyTokenString ($ token )) {
353+ return $ token ;
329354 }
355+ }
330356
331- parse_str ($ body , $ parsed );
332- $ tokenValue = $ parsed [$ this ->config ->tokenName ] ?? null ;
357+ // 3b. Check if form-encoded data exists and contains the token.
358+ parse_str ($ body , $ result );
359+ $ token = $ result [$ tokenName ] ?? null ;
333360
334- return is_string ($ tokenValue ) ? $ tokenValue : null ;
361+ if ($ this ->isNonEmptyTokenString ($ token )) {
362+ return $ token ;
335363 }
336364
337365 return null ;
338366 }
339367
368+ /**
369+ * @phpstan-assert-if-true non-empty-string $token
370+ */
371+ private function isNonEmptyTokenString (mixed $ token ): bool
372+ {
373+ return is_string ($ token ) && $ token !== '' ;
374+ }
375+
340376 /**
341377 * Returns the CSRF Token.
342378 */
0 commit comments