Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
66.67% |
22 / 33 |
|
50.00% |
3 / 6 |
CRAP | |
0.00% |
0 / 1 |
TokenAwareHandlerTrait | |
66.67% |
22 / 33 |
|
50.00% |
3 / 6 |
27.70 | |
0.00% |
0 / 1 |
getValidatedBody | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
getSession | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
getTokenParamDefinition | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
getToken | |
66.67% |
4 / 6 |
|
0.00% |
0 / 1 |
3.33 | |||
needsToken | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getBadTokenMessage | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
validateToken | |
93.75% |
15 / 16 |
|
0.00% |
0 / 1 |
10.02 | |||
getBadTokenException | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Rest; |
4 | |
5 | use LogicException; |
6 | use MediaWiki\Session\Session; |
7 | use MediaWiki\User\LoggedOutEditToken; |
8 | use Wikimedia\Message\DataMessageValue; |
9 | use Wikimedia\Message\MessageValue; |
10 | use Wikimedia\ParamValidator\ParamValidator; |
11 | |
12 | /** |
13 | * This trait can be used on handlers that choose to support token-based CSRF protection. Note that doing so is |
14 | * discouraged, and you should preferably require that the endpoint be used with a session provider that is |
15 | * safe against CSRF, such as OAuth. |
16 | * @see Handler::requireSafeAgainstCsrf() |
17 | * |
18 | * @package MediaWiki\Rest |
19 | */ |
20 | trait TokenAwareHandlerTrait { |
21 | abstract public function getValidatedBody(); |
22 | |
23 | abstract public function getSession(): Session; |
24 | |
25 | /** |
26 | * Returns the definition for the token parameter, to be used in getBodyValidator(). |
27 | * |
28 | * @return array[] |
29 | */ |
30 | protected function getTokenParamDefinition(): array { |
31 | return [ |
32 | 'token' => [ |
33 | Handler::PARAM_SOURCE => 'body', |
34 | ParamValidator::PARAM_TYPE => 'string', |
35 | ParamValidator::PARAM_REQUIRED => false, |
36 | ParamValidator::PARAM_DEFAULT => '', |
37 | ] |
38 | ]; |
39 | } |
40 | |
41 | /** |
42 | * Determines the CSRF token to be used, possibly taking it from a request parameter. |
43 | * |
44 | * Returns an empty string if the request isn't known to be safe and |
45 | * no token was supplied by the client. |
46 | * Returns null if the session provider is safe against CSRF (and thus no token |
47 | * is needed) |
48 | * |
49 | * @return string|null |
50 | */ |
51 | protected function getToken(): ?string { |
52 | if ( !$this instanceof Handler ) { |
53 | throw new LogicException( 'This trait must be used on handler classes.' ); |
54 | } |
55 | |
56 | if ( !$this->needsToken() ) { |
57 | return null; |
58 | } |
59 | |
60 | $body = $this->getValidatedBody(); |
61 | return $body['token'] ?? ''; |
62 | } |
63 | |
64 | /** |
65 | * Determines whether a CSRF token is needed. |
66 | * |
67 | * Returns false if the request has been authenticated in a way that |
68 | * protects against CSRF, such as OAuth. |
69 | * |
70 | * @return bool |
71 | */ |
72 | protected function needsToken(): bool { |
73 | return !$this->getSession()->getProvider()->safeAgainstCsrf(); |
74 | } |
75 | |
76 | /** |
77 | * Returns a standard error message to use when the given CSRF token is invalid. |
78 | * In the future, this trait may also provide a method for checking the token. |
79 | * |
80 | * @return MessageValue |
81 | */ |
82 | protected function getBadTokenMessage(): MessageValue { |
83 | return DataMessageValue::new( 'rest-badtoken' ); |
84 | } |
85 | |
86 | /** |
87 | * Checks that the given CSRF token is valid (or the used authentication method does |
88 | * not require CSRF). |
89 | * Note that this method only supports the 'csrf' token type. The body validator must |
90 | * return an array and include the 'token' field (see getTokenParamDefinition()). |
91 | * @param bool $allowAnonymousToken Allow anonymous users to pass the check by submitting |
92 | * an empty token. (This matches how e.g. anonymous editing works on the action API and web.) |
93 | * @return void |
94 | * @throws LocalizedHttpException |
95 | */ |
96 | protected function validateToken( bool $allowAnonymousToken = false ): void { |
97 | if ( $this->getSession()->getProvider()->safeAgainstCsrf() ) { |
98 | return; |
99 | } |
100 | |
101 | $submittedToken = $this->getToken(); |
102 | $sessionToken = null; |
103 | $isAnon = $this->getSession()->getUser()->isAnon(); |
104 | if ( $allowAnonymousToken && $isAnon ) { |
105 | $sessionToken = new LoggedOutEditToken(); |
106 | } elseif ( $this->getSession()->hasToken() ) { |
107 | $sessionToken = $this->getSession()->getToken(); |
108 | } |
109 | |
110 | if ( $sessionToken && $sessionToken->match( $submittedToken ) ) { |
111 | return; |
112 | } elseif ( !$submittedToken ) { |
113 | throw $this->getBadTokenException( 'rest-badtoken-missing' ); |
114 | } elseif ( $isAnon && !$this->getSession()->isPersistent() ) { |
115 | // The client probably forgot to authenticate. |
116 | throw $this->getBadTokenException( 'rest-badtoken-nosession' ); |
117 | } else { |
118 | // The user submitted a token, the session had a token, but they didn't match. |
119 | throw new LocalizedHttpException( $this->getBadTokenMessage(), 403 ); |
120 | } |
121 | } |
122 | |
123 | /** |
124 | * @param string $messageKey |
125 | * @return LocalizedHttpException |
126 | * @internal For use by the trait only |
127 | */ |
128 | private function getBadTokenException( string $messageKey ): LocalizedHttpException { |
129 | return new LocalizedHttpException( DataMessageValue::new( $messageKey, [], 'rest-badtoken' ), 403 ); |
130 | } |
131 | } |