@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
at recaptime-dev/main 267 lines 8.0 kB view raw
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}