Something went wrong. Try again.
@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
Something went wrong. Try again.
8.0 kB · 267 lines
PHP
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268<?php
/** * Guard writes against CSRF. The Aphront structure takes care of most of this * for you, you just need to call: * * AphrontWriteGuard::willWrite(); * * ...before executing a write against any new kind of storage engine. MySQL * databases and the default file storage engines are already covered, but if * you introduce new types of datastores make sure their writes are guarded. If * you don't guard writes and make a mistake doing CSRF checks in a controller, * a CSRF vulnerability can escape undetected. * * If you need to execute writes on a page which doesn't have CSRF tokens (for * example, because you need to do logging), you can temporarily disable the * write guard by calling: * * AphrontWriteGuard::beginUnguardedWrites(); * do_logging_write(); * AphrontWriteGuard::endUnguardedWrites(); * * This is dangerous, because it disables the backup layer of CSRF protection * this class provides. You should need this only very, very rarely. * * @task protect Protecting Writes * @task disable Disabling Protection * @task manage Managing Write Guards * @task internal Internals */final class AphrontWriteGuard extends Phobject {
private static $instance; private static $allowUnguardedWrites = false;
private $callback; private $allowDepth = 0;
/* -( Managing Write Guards )---------------------------------------------- */
/** * Construct a new write guard for a request. Only one write guard may be * active at a time. You must explicitly call @{method:dispose} when you are * done with a write guard: * * $guard = new AphrontWriteGuard($callback); * // ... * $guard->dispose(); * * Normally, you do not need to manage guards yourself -- the Aphront stack * handles it for you. * * This class accepts a callback, which will be invoked when a write is * attempted. The callback should validate the presence of a CSRF token in * the request, or abort the request (e.g., by throwing an exception) if a * valid token isn't present. * * @param $callback Callable CSRF callback. * @return $this * @task manage */ public function __construct($callback) { if (self::$instance) { throw new Exception( pht( 'An %s already exists. Dispose of the previous guard '. 'before creating a new one.', self::class)); } if (self::$allowUnguardedWrites) { throw new Exception( pht( 'An %s is being created in a context which permits '. 'unguarded writes unconditionally. This is not allowed and '. 'indicates a serious error.', self::class)); } $this->callback = $callback; self::$instance = $this; }
/** * Dispose of the active write guard. You must call this method when you are * done with a write guard. You do not normally need to call this yourself. * * @return void * @task manage */ public function dispose() { if (!self::$instance) { throw new Exception(pht( 'Attempting to dispose of write guard, but no write guard is active!')); }
if ($this->allowDepth > 0) { throw new Exception( pht( 'Imbalanced %s: more %s calls than %s calls.', self::class, 'beginUnguardedWrites()', 'endUnguardedWrites()')); } self::$instance = null; }
/** * Determine if there is an active write guard. * * @return bool * @task manage */ public static function isGuardActive() { return (bool)self::$instance; }
/** * Return on instance of AphrontWriteGuard if it's active, or null * * @return AphrontWriteGuard|null */ public static function getInstance() { return self::$instance; }
/* -( Protecting Writes )-------------------------------------------------- */
/** * Declare intention to perform a write, validating that writes are allowed. * You should call this method before executing a write whenever you implement * a new storage engine where information can be permanently kept. * * Writes are permitted if: * * - The request has valid CSRF tokens. * - Unguarded writes have been temporarily enabled by a call to * @{method:beginUnguardedWrites}. * - All write guarding has been disabled with * @{method:allowDangerousUnguardedWrites}. * * If none of these conditions are true, this method will throw and prevent * the write. * * @return void * @task protect */ public static function willWrite() { if (!self::$instance) { if (!self::$allowUnguardedWrites) { throw new Exception( pht( 'Unguarded write! There must be an active %s to perform writes.', self::class)); } else { // Unguarded writes are being allowed unconditionally. return; } }
$instance = self::$instance; if ($instance->allowDepth == 0) { call_user_func($instance->callback); } }
/* -( Disabling Write Protection )----------------------------------------- */
/** * Enter a scope which permits unguarded writes. This works like * @{method:beginUnguardedWrites} but returns an object which will end * the unguarded write scope when its __destruct() method is called. This * is useful to more easily handle exceptions correctly in unguarded write * blocks: * * // Restores the guard even if do_logging() throws. * function unguarded_scope() { * $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); * do_logging(); * } * * @return AphrontScopedUnguardedWriteCapability Object which ends unguarded * writes when it leaves scope. * @task disable */ public static function beginScopedUnguardedWrites() { self::beginUnguardedWrites(); return new AphrontScopedUnguardedWriteCapability(); }
/** * Begin a block which permits unguarded writes. You should use this very * sparingly, and only for things like logging where CSRF is not a concern. * * You must pair every call to @{method:beginUnguardedWrites} with a call to * @{method:endUnguardedWrites}: * * AphrontWriteGuard::beginUnguardedWrites(); * do_logging(); * AphrontWriteGuard::endUnguardedWrites(); * * @return void * @task disable */ public static function beginUnguardedWrites() { if (!self::$instance) { return; } self::$instance->allowDepth++; }
/** * Declare that you have finished performing unguarded writes. You must * call this exactly once for each call to @{method:beginUnguardedWrites}. * * @return void * @task disable */ public static function endUnguardedWrites() { if (!self::$instance) { return; } if (self::$instance->allowDepth <= 0) { throw new Exception( pht( 'Imbalanced %s: more %s calls than %s calls.', self::class, 'endUnguardedWrites()', 'beginUnguardedWrites()')); } self::$instance->allowDepth--; }
/** * Allow execution of unguarded writes. This is ONLY appropriate for use in * script contexts or other contexts where you are guaranteed to never be * vulnerable to CSRF concerns. Calling this method is EXTREMELY DANGEROUS * if you do not understand the consequences. * * If you need to perform unguarded writes on an otherwise guarded workflow * which is vulnerable to CSRF, use @{method:beginUnguardedWrites}. * * @return void * @task disable */ public static function allowDangerousUnguardedWrites($allow) { if (self::$instance) { throw new Exception( pht( 'You can not unconditionally disable %s by calling %s while a write '. 'guard is active. Use %s to temporarily allow unguarded writes.', self::class, __FUNCTION__.'()', 'beginUnguardedWrites()')); } self::$allowUnguardedWrites = true; }
}