Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/aphront/writeguard/AphrontWriteGuard.php
12241 views
1
<?php
2
3
/**
4
* Guard writes against CSRF. The Aphront structure takes care of most of this
5
* for you, you just need to call:
6
*
7
* AphrontWriteGuard::willWrite();
8
*
9
* ...before executing a write against any new kind of storage engine. MySQL
10
* databases and the default file storage engines are already covered, but if
11
* you introduce new types of datastores make sure their writes are guarded. If
12
* you don't guard writes and make a mistake doing CSRF checks in a controller,
13
* a CSRF vulnerability can escape undetected.
14
*
15
* If you need to execute writes on a page which doesn't have CSRF tokens (for
16
* example, because you need to do logging), you can temporarily disable the
17
* write guard by calling:
18
*
19
* AphrontWriteGuard::beginUnguardedWrites();
20
* do_logging_write();
21
* AphrontWriteGuard::endUnguardedWrites();
22
*
23
* This is dangerous, because it disables the backup layer of CSRF protection
24
* this class provides. You should need this only very, very rarely.
25
*
26
* @task protect Protecting Writes
27
* @task disable Disabling Protection
28
* @task manage Managing Write Guards
29
* @task internal Internals
30
*/
31
final class AphrontWriteGuard extends Phobject {
32
33
private static $instance;
34
private static $allowUnguardedWrites = false;
35
36
private $callback;
37
private $allowDepth = 0;
38
39
40
/* -( Managing Write Guards )---------------------------------------------- */
41
42
43
/**
44
* Construct a new write guard for a request. Only one write guard may be
45
* active at a time. You must explicitly call @{method:dispose} when you are
46
* done with a write guard:
47
*
48
* $guard = new AphrontWriteGuard($callback);
49
* // ...
50
* $guard->dispose();
51
*
52
* Normally, you do not need to manage guards yourself -- the Aphront stack
53
* handles it for you.
54
*
55
* This class accepts a callback, which will be invoked when a write is
56
* attempted. The callback should validate the presence of a CSRF token in
57
* the request, or abort the request (e.g., by throwing an exception) if a
58
* valid token isn't present.
59
*
60
* @param callable CSRF callback.
61
* @return this
62
* @task manage
63
*/
64
public function __construct($callback) {
65
if (self::$instance) {
66
throw new Exception(
67
pht(
68
'An %s already exists. Dispose of the previous guard '.
69
'before creating a new one.',
70
__CLASS__));
71
}
72
if (self::$allowUnguardedWrites) {
73
throw new Exception(
74
pht(
75
'An %s is being created in a context which permits '.
76
'unguarded writes unconditionally. This is not allowed and '.
77
'indicates a serious error.',
78
__CLASS__));
79
}
80
$this->callback = $callback;
81
self::$instance = $this;
82
}
83
84
85
/**
86
* Dispose of the active write guard. You must call this method when you are
87
* done with a write guard. You do not normally need to call this yourself.
88
*
89
* @return void
90
* @task manage
91
*/
92
public function dispose() {
93
if (!self::$instance) {
94
throw new Exception(pht(
95
'Attempting to dispose of write guard, but no write guard is active!'));
96
}
97
98
if ($this->allowDepth > 0) {
99
throw new Exception(
100
pht(
101
'Imbalanced %s: more %s calls than %s calls.',
102
__CLASS__,
103
'beginUnguardedWrites()',
104
'endUnguardedWrites()'));
105
}
106
self::$instance = null;
107
}
108
109
110
/**
111
* Determine if there is an active write guard.
112
*
113
* @return bool
114
* @task manage
115
*/
116
public static function isGuardActive() {
117
return (bool)self::$instance;
118
}
119
120
/**
121
* Return on instance of AphrontWriteGuard if it's active, or null
122
*
123
* @return AphrontWriteGuard|null
124
*/
125
public static function getInstance() {
126
return self::$instance;
127
}
128
129
130
/* -( Protecting Writes )-------------------------------------------------- */
131
132
133
/**
134
* Declare intention to perform a write, validating that writes are allowed.
135
* You should call this method before executing a write whenever you implement
136
* a new storage engine where information can be permanently kept.
137
*
138
* Writes are permitted if:
139
*
140
* - The request has valid CSRF tokens.
141
* - Unguarded writes have been temporarily enabled by a call to
142
* @{method:beginUnguardedWrites}.
143
* - All write guarding has been disabled with
144
* @{method:allowDangerousUnguardedWrites}.
145
*
146
* If none of these conditions are true, this method will throw and prevent
147
* the write.
148
*
149
* @return void
150
* @task protect
151
*/
152
public static function willWrite() {
153
if (!self::$instance) {
154
if (!self::$allowUnguardedWrites) {
155
throw new Exception(
156
pht(
157
'Unguarded write! There must be an active %s to perform writes.',
158
__CLASS__));
159
} else {
160
// Unguarded writes are being allowed unconditionally.
161
return;
162
}
163
}
164
165
$instance = self::$instance;
166
if ($instance->allowDepth == 0) {
167
call_user_func($instance->callback);
168
}
169
}
170
171
172
/* -( Disabling Write Protection )----------------------------------------- */
173
174
175
/**
176
* Enter a scope which permits unguarded writes. This works like
177
* @{method:beginUnguardedWrites} but returns an object which will end
178
* the unguarded write scope when its __destruct() method is called. This
179
* is useful to more easily handle exceptions correctly in unguarded write
180
* blocks:
181
*
182
* // Restores the guard even if do_logging() throws.
183
* function unguarded_scope() {
184
* $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
185
* do_logging();
186
* }
187
*
188
* @return AphrontScopedUnguardedWriteCapability Object which ends unguarded
189
* writes when it leaves scope.
190
* @task disable
191
*/
192
public static function beginScopedUnguardedWrites() {
193
self::beginUnguardedWrites();
194
return new AphrontScopedUnguardedWriteCapability();
195
}
196
197
198
/**
199
* Begin a block which permits unguarded writes. You should use this very
200
* sparingly, and only for things like logging where CSRF is not a concern.
201
*
202
* You must pair every call to @{method:beginUnguardedWrites} with a call to
203
* @{method:endUnguardedWrites}:
204
*
205
* AphrontWriteGuard::beginUnguardedWrites();
206
* do_logging();
207
* AphrontWriteGuard::endUnguardedWrites();
208
*
209
* @return void
210
* @task disable
211
*/
212
public static function beginUnguardedWrites() {
213
if (!self::$instance) {
214
return;
215
}
216
self::$instance->allowDepth++;
217
}
218
219
/**
220
* Declare that you have finished performing unguarded writes. You must
221
* call this exactly once for each call to @{method:beginUnguardedWrites}.
222
*
223
* @return void
224
* @task disable
225
*/
226
public static function endUnguardedWrites() {
227
if (!self::$instance) {
228
return;
229
}
230
if (self::$instance->allowDepth <= 0) {
231
throw new Exception(
232
pht(
233
'Imbalanced %s: more %s calls than %s calls.',
234
__CLASS__,
235
'endUnguardedWrites()',
236
'beginUnguardedWrites()'));
237
}
238
self::$instance->allowDepth--;
239
}
240
241
242
/**
243
* Allow execution of unguarded writes. This is ONLY appropriate for use in
244
* script contexts or other contexts where you are guaranteed to never be
245
* vulnerable to CSRF concerns. Calling this method is EXTREMELY DANGEROUS
246
* if you do not understand the consequences.
247
*
248
* If you need to perform unguarded writes on an otherwise guarded workflow
249
* which is vulnerable to CSRF, use @{method:beginUnguardedWrites}.
250
*
251
* @return void
252
* @task disable
253
*/
254
public static function allowDangerousUnguardedWrites($allow) {
255
if (self::$instance) {
256
throw new Exception(
257
pht(
258
'You can not unconditionally disable %s by calling %s while a write '.
259
'guard is active. Use %s to temporarily allow unguarded writes.',
260
__CLASS__,
261
__FUNCTION__.'()',
262
'beginUnguardedWrites()'));
263
}
264
self::$allowUnguardedWrites = true;
265
}
266
267
}
268
269