File: TokenAwareHandlerTrait.php

package info (click to toggle)
mediawiki 1%3A1.43.3%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 417,464 kB
  • sloc: php: 1,062,949; javascript: 664,290; sql: 9,714; python: 5,458; xml: 3,489; sh: 1,131; makefile: 64
file content (131 lines) | stat: -rw-r--r-- 4,178 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
<?php

namespace MediaWiki\Rest;

use LogicException;
use MediaWiki\Session\Session;
use MediaWiki\User\LoggedOutEditToken;
use Wikimedia\Message\DataMessageValue;
use Wikimedia\Message\MessageValue;
use Wikimedia\ParamValidator\ParamValidator;

/**
 * This trait can be used on handlers that choose to support token-based CSRF protection. Note that doing so is
 * discouraged, and you should preferably require that the endpoint be used with a session provider that is
 * safe against CSRF, such as OAuth.
 * @see Handler::requireSafeAgainstCsrf()
 *
 * @package MediaWiki\Rest
 */
trait TokenAwareHandlerTrait {
	abstract public function getValidatedBody();

	abstract public function getSession(): Session;

	/**
	 * Returns the definition for the token parameter, to be used in getBodyValidator().
	 *
	 * @return array[]
	 */
	protected function getTokenParamDefinition(): array {
		return [
			'token' => [
				Handler::PARAM_SOURCE => 'body',
				ParamValidator::PARAM_TYPE => 'string',
				ParamValidator::PARAM_REQUIRED => false,
				ParamValidator::PARAM_DEFAULT => '',
			]
		];
	}

	/**
	 * Determines the CSRF token to be used, possibly taking it from a request parameter.
	 *
	 * Returns an empty string if the request isn't known to be safe and
	 * no token was supplied by the client.
	 * Returns null if the session provider is safe against CSRF (and thus no token
	 * is needed)
	 *
	 * @return string|null
	 */
	protected function getToken(): ?string {
		if ( !$this instanceof Handler ) {
			throw new LogicException( 'This trait must be used on handler classes.' );
		}

		if ( !$this->needsToken() ) {
			return null;
		}

		$body = $this->getValidatedBody();
		return $body['token'] ?? '';
	}

	/**
	 * Determines whether a CSRF token is needed.
	 *
	 * Returns false if the request has been authenticated in a way that
	 * protects against CSRF, such as OAuth.
	 *
	 * @return bool
	 */
	protected function needsToken(): bool {
		return !$this->getSession()->getProvider()->safeAgainstCsrf();
	}

	/**
	 * Returns a standard error message to use when the given CSRF token is invalid.
	 * In the future, this trait may also provide a method for checking the token.
	 *
	 * @return MessageValue
	 */
	protected function getBadTokenMessage(): MessageValue {
		return DataMessageValue::new( 'rest-badtoken' );
	}

	/**
	 * Checks that the given CSRF token is valid (or the used authentication method does
	 * not require CSRF).
	 * Note that this method only supports the 'csrf' token type. The body validator must
	 * return an array and include the 'token' field (see getTokenParamDefinition()).
	 * @param bool $allowAnonymousToken Allow anonymous users to pass the check by submitting
	 *   an empty token. (This matches how e.g. anonymous editing works on the action API and web.)
	 * @return void
	 * @throws LocalizedHttpException
	 */
	protected function validateToken( bool $allowAnonymousToken = false ): void {
		if ( $this->getSession()->getProvider()->safeAgainstCsrf() ) {
			return;
		}

		$submittedToken = $this->getToken();
		$sessionToken = null;
		$isAnon = $this->getSession()->getUser()->isAnon();
		if ( $allowAnonymousToken && $isAnon ) {
			$sessionToken = new LoggedOutEditToken();
		} elseif ( $this->getSession()->hasToken() ) {
			$sessionToken = $this->getSession()->getToken();
		}

		if ( $sessionToken && $sessionToken->match( $submittedToken ) ) {
			return;
		} elseif ( !$submittedToken ) {
			throw $this->getBadTokenException( 'rest-badtoken-missing' );
		} elseif ( $isAnon && !$this->getSession()->isPersistent() ) {
			// The client probably forgot to authenticate.
			throw $this->getBadTokenException( 'rest-badtoken-nosession' );
		} else {
			// The user submitted a token, the session had a token, but they didn't match.
			throw new LocalizedHttpException( $this->getBadTokenMessage(), 403 );
		}
	}

	/**
	 * @param string $messageKey
	 * @return LocalizedHttpException
	 * @internal For use by the trait only
	 */
	private function getBadTokenException( string $messageKey ): LocalizedHttpException {
		return new LocalizedHttpException( DataMessageValue::new( $messageKey, [], 'rest-badtoken' ), 403 );
	}
}