Path: blob/master/src/aphront/writeguard/AphrontWriteGuard.php
12241 views
<?php12/**3* Guard writes against CSRF. The Aphront structure takes care of most of this4* for you, you just need to call:5*6* AphrontWriteGuard::willWrite();7*8* ...before executing a write against any new kind of storage engine. MySQL9* databases and the default file storage engines are already covered, but if10* you introduce new types of datastores make sure their writes are guarded. If11* you don't guard writes and make a mistake doing CSRF checks in a controller,12* a CSRF vulnerability can escape undetected.13*14* If you need to execute writes on a page which doesn't have CSRF tokens (for15* example, because you need to do logging), you can temporarily disable the16* write guard by calling:17*18* AphrontWriteGuard::beginUnguardedWrites();19* do_logging_write();20* AphrontWriteGuard::endUnguardedWrites();21*22* This is dangerous, because it disables the backup layer of CSRF protection23* this class provides. You should need this only very, very rarely.24*25* @task protect Protecting Writes26* @task disable Disabling Protection27* @task manage Managing Write Guards28* @task internal Internals29*/30final class AphrontWriteGuard extends Phobject {3132private static $instance;33private static $allowUnguardedWrites = false;3435private $callback;36private $allowDepth = 0;373839/* -( Managing Write Guards )---------------------------------------------- */404142/**43* Construct a new write guard for a request. Only one write guard may be44* active at a time. You must explicitly call @{method:dispose} when you are45* done with a write guard:46*47* $guard = new AphrontWriteGuard($callback);48* // ...49* $guard->dispose();50*51* Normally, you do not need to manage guards yourself -- the Aphront stack52* handles it for you.53*54* This class accepts a callback, which will be invoked when a write is55* attempted. The callback should validate the presence of a CSRF token in56* the request, or abort the request (e.g., by throwing an exception) if a57* valid token isn't present.58*59* @param callable CSRF callback.60* @return this61* @task manage62*/63public function __construct($callback) {64if (self::$instance) {65throw new Exception(66pht(67'An %s already exists. Dispose of the previous guard '.68'before creating a new one.',69__CLASS__));70}71if (self::$allowUnguardedWrites) {72throw new Exception(73pht(74'An %s is being created in a context which permits '.75'unguarded writes unconditionally. This is not allowed and '.76'indicates a serious error.',77__CLASS__));78}79$this->callback = $callback;80self::$instance = $this;81}828384/**85* Dispose of the active write guard. You must call this method when you are86* done with a write guard. You do not normally need to call this yourself.87*88* @return void89* @task manage90*/91public function dispose() {92if (!self::$instance) {93throw new Exception(pht(94'Attempting to dispose of write guard, but no write guard is active!'));95}9697if ($this->allowDepth > 0) {98throw new Exception(99pht(100'Imbalanced %s: more %s calls than %s calls.',101__CLASS__,102'beginUnguardedWrites()',103'endUnguardedWrites()'));104}105self::$instance = null;106}107108109/**110* Determine if there is an active write guard.111*112* @return bool113* @task manage114*/115public static function isGuardActive() {116return (bool)self::$instance;117}118119/**120* Return on instance of AphrontWriteGuard if it's active, or null121*122* @return AphrontWriteGuard|null123*/124public static function getInstance() {125return self::$instance;126}127128129/* -( Protecting Writes )-------------------------------------------------- */130131132/**133* Declare intention to perform a write, validating that writes are allowed.134* You should call this method before executing a write whenever you implement135* a new storage engine where information can be permanently kept.136*137* Writes are permitted if:138*139* - The request has valid CSRF tokens.140* - Unguarded writes have been temporarily enabled by a call to141* @{method:beginUnguardedWrites}.142* - All write guarding has been disabled with143* @{method:allowDangerousUnguardedWrites}.144*145* If none of these conditions are true, this method will throw and prevent146* the write.147*148* @return void149* @task protect150*/151public static function willWrite() {152if (!self::$instance) {153if (!self::$allowUnguardedWrites) {154throw new Exception(155pht(156'Unguarded write! There must be an active %s to perform writes.',157__CLASS__));158} else {159// Unguarded writes are being allowed unconditionally.160return;161}162}163164$instance = self::$instance;165if ($instance->allowDepth == 0) {166call_user_func($instance->callback);167}168}169170171/* -( Disabling Write Protection )----------------------------------------- */172173174/**175* Enter a scope which permits unguarded writes. This works like176* @{method:beginUnguardedWrites} but returns an object which will end177* the unguarded write scope when its __destruct() method is called. This178* is useful to more easily handle exceptions correctly in unguarded write179* blocks:180*181* // Restores the guard even if do_logging() throws.182* function unguarded_scope() {183* $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();184* do_logging();185* }186*187* @return AphrontScopedUnguardedWriteCapability Object which ends unguarded188* writes when it leaves scope.189* @task disable190*/191public static function beginScopedUnguardedWrites() {192self::beginUnguardedWrites();193return new AphrontScopedUnguardedWriteCapability();194}195196197/**198* Begin a block which permits unguarded writes. You should use this very199* sparingly, and only for things like logging where CSRF is not a concern.200*201* You must pair every call to @{method:beginUnguardedWrites} with a call to202* @{method:endUnguardedWrites}:203*204* AphrontWriteGuard::beginUnguardedWrites();205* do_logging();206* AphrontWriteGuard::endUnguardedWrites();207*208* @return void209* @task disable210*/211public static function beginUnguardedWrites() {212if (!self::$instance) {213return;214}215self::$instance->allowDepth++;216}217218/**219* Declare that you have finished performing unguarded writes. You must220* call this exactly once for each call to @{method:beginUnguardedWrites}.221*222* @return void223* @task disable224*/225public static function endUnguardedWrites() {226if (!self::$instance) {227return;228}229if (self::$instance->allowDepth <= 0) {230throw new Exception(231pht(232'Imbalanced %s: more %s calls than %s calls.',233__CLASS__,234'endUnguardedWrites()',235'beginUnguardedWrites()'));236}237self::$instance->allowDepth--;238}239240241/**242* Allow execution of unguarded writes. This is ONLY appropriate for use in243* script contexts or other contexts where you are guaranteed to never be244* vulnerable to CSRF concerns. Calling this method is EXTREMELY DANGEROUS245* if you do not understand the consequences.246*247* If you need to perform unguarded writes on an otherwise guarded workflow248* which is vulnerable to CSRF, use @{method:beginUnguardedWrites}.249*250* @return void251* @task disable252*/253public static function allowDangerousUnguardedWrites($allow) {254if (self::$instance) {255throw new Exception(256pht(257'You can not unconditionally disable %s by calling %s while a write '.258'guard is active. Use %s to temporarily allow unguarded writes.',259__CLASS__,260__FUNCTION__.'()',261'beginUnguardedWrites()'));262}263self::$allowUnguardedWrites = true;264}265266}267268269