@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
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 */
31final 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 $callback 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 self::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 self::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 self::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 self::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 self::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 self::class,
261 __FUNCTION__.'()',
262 'beginUnguardedWrites()'));
263 }
264 self::$allowUnguardedWrites = true;
265 }
266
267}