diff --git a/src/aphront/writeguard/AphrontWriteGuard.php b/src/aphront/writeguard/AphrontWriteGuard.php index d5c55e2..301bb20 100644 --- a/src/aphront/writeguard/AphrontWriteGuard.php +++ b/src/aphront/writeguard/AphrontWriteGuard.php @@ -1,305 +1,308 @@ <?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 { private static $instance; private static $allowUnguardedWrites = false; private static $abruptExitlistenerIsInstalled = false; private $callback; private $allowDepth = 0; + private $disposed; /* -( 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 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.', __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.', __CLASS__)); } if (!self::$abruptExitlistenerIsInstalled) { self::$abruptExitlistenerIsInstalled = true; $event_listener = new AphrontWriteGuardExitEventListener(); $event_listener->register(); } $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 % calls than %s calls.', __CLASS__, 'beginUnguardedWrites()', 'endUnguardedWrites()')); } + $this->disposed = true; self::$instance = null; } /** * This is used for clearing the write guard without performing any checks. * This is used in conjunction with phutil_exit for abrupt exits. * * @return void */ public function disposeAbruptly() { + $this->disposed = true; 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.', __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.', __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.', __CLASS__, __FUNCTION__.'()', 'beginUnguardedWrites()')); } self::$allowUnguardedWrites = true; } /* -( Internals )---------------------------------------------------------- */ /** * When the object is destroyed, make sure @{method:dispose} was called. * * @task internal */ public function __destruct() { - if (isset(self::$instance)) { + if (!$this->disposed) { throw new Exception( pht( '%s was not properly disposed of! Call %s on every %s object you '. 'instantiate or use %s to exit abruptly while debugging.', __CLASS__, 'dispose()', __CLASS__, 'phutil_exit()')); } } }