diff --git a/src/aphront/writeguard/AphrontWriteGuard.php b/src/aphront/writeguard/AphrontWriteGuard.php index e0a61284cf..257318cca1 100644 --- a/src/aphront/writeguard/AphrontWriteGuard.php +++ b/src/aphront/writeguard/AphrontWriteGuard.php @@ -1,267 +1,273 @@ dispose(); * * Normally, you do not need to manage guards yourself -- the Aphront stack * handles it for you. * - * @param AphrontRequest Request to read CSRF token information from. + * 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(AphrontRequest $request) { + public function __construct($callback) { if (self::$instance) { throw new Exception( "An AphrontWriteGuard already exists. Dispose of the previous guard ". "before creating a new one."); } if (self::$allowUnguardedWrites) { throw new Exception( "An AphrontWriteGuard is being created in a context which permits ". "unguarded writes unconditionally. This is not allowed and indicates ". "a serious error."); } - $this->request = $request; + $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 ($this->allowDepth > 0) { throw new Exception( "Imbalanced AphrontWriteGuard: more beginUnguardedWrites() calls than ". "endUnguardedWrites() calls."); } self::$instance = null; } /** * Determine if there is an active write guard. * * @return bool * @task manage */ public static function isGuardActive() { return (bool)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( "Unguarded write! There must be an active AphrontWriteGuard to ". "perform writes."); } else { // Unguarded writes are being allowed unconditionally. return; } } $instance = self::$instance; - if ($instance->allowDepth == 0) { - $instance->request->validateCSRF(); + 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( "Imbalanced AphrontWriteGuard: more endUnguardedWrites() calls than ". "beginUnguardedWrites() calls."); } 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( "You can not unconditionally disable AphrontWriteGuard by calling ". "allowDangerousUnguardedWrites() while a write guard is active. Use ". "beginUnguardedWrites() to temporarily allow unguarded writes."); } self::$allowUnguardedWrites = true; } /* -( Internals )---------------------------------------------------------- */ /** * When the object is destroyed, make sure @{method:dispose} was called. + * + * @task internal */ public function __destruct() { if (isset(self::$instance)) { throw new Exception( "AphrontWriteGuard was not properly disposed of! Call dispose() on ". "every AphrontWriteGuard object you instantiate."); } } } diff --git a/webroot/index.php b/webroot/index.php index cfca18e57a..31c0d4d939 100644 --- a/webroot/index.php +++ b/webroot/index.php @@ -1,392 +1,392 @@ ', where '' ". "is one of 'development', 'production', or a custom environment."); } if (!isset($_REQUEST['__path__'])) { phabricator_fatal_config_error( "__path__ is not set. Your rewrite rules are not configured correctly."); } if (get_magic_quotes_gpc()) { phabricator_fatal_config_error( "Your server is configured with PHP 'magic_quotes_gpc' enabled. This ". "feature is 'highly discouraged' by PHP's developers and you must ". "disable it to run Phabricator. Consult the PHP manual for instructions."); } register_shutdown_function('phabricator_shutdown'); require_once dirname(dirname(__FILE__)).'/conf/__init_conf__.php'; try { setup_aphront_basics(); $conf = phabricator_read_config_file($env); $conf['phabricator.env'] = $env; PhabricatorEnv::setEnvConfig($conf); // This needs to be done before we create the log, because // PhabricatorAccessLog::getLog() calls date() $tz = PhabricatorEnv::getEnvConfig('phabricator.timezone'); if ($tz) { date_default_timezone_set($tz); } // This is the earliest we can get away with this, we need env config first. PhabricatorAccessLog::init(); $access_log = PhabricatorAccessLog::getLog(); if ($access_log) { $access_log->setData( array( 'R' => idx($_SERVER, 'HTTP_REFERER', '-'), 'r' => idx($_SERVER, 'REMOTE_ADDR', '-'), 'M' => idx($_SERVER, 'REQUEST_METHOD', '-'), )); } DarkConsoleXHProfPluginAPI::hookProfiler(); PhutilErrorHandler::initialize(); } catch (Exception $ex) { phabricator_fatal("[Initialization Exception] ".$ex->getMessage()); } PhutilErrorHandler::setErrorListener( array('DarkConsoleErrorLogPluginAPI', 'handleErrors')); foreach (PhabricatorEnv::getEnvConfig('load-libraries') as $library) { phutil_load_library($library); } if (PhabricatorEnv::getEnvConfig('phabricator.setup')) { try { PhabricatorSetup::runSetup(); } catch (Exception $ex) { echo "EXCEPTION!\n"; echo $ex; } return; } phabricator_detect_bad_base_uri(); $translation = PhabricatorEnv::newObjectFromConfig('translation.provider'); PhutilTranslator::getInstance() ->setLanguage($translation->getLanguage()) ->addTranslations($translation->getTranslations()); $host = $_SERVER['HTTP_HOST']; $path = $_REQUEST['__path__']; switch ($host) { default: $config_key = 'aphront.default-application-configuration-class'; $application = PhabricatorEnv::newObjectFromConfig($config_key); break; } $application->setHost($host); $application->setPath($path); $application->willBuildRequest(); $request = $application->buildRequest(); -$write_guard = new AphrontWriteGuard($request); +$write_guard = new AphrontWriteGuard(array($request, 'validateCSRF')); PhabricatorEventEngine::initialize(); $application->setRequest($request); list($controller, $uri_data) = $application->buildController(); if ($access_log) { $access_log->setData( array( 'U' => (string)$request->getRequestURI()->getPath(), 'C' => get_class($controller), )); } // If execution throws an exception and then trying to render that exception // throws another exception, we want to show the original exception, as it is // likely the root cause of the rendering exception. $original_exception = null; try { $response = $controller->willBeginExecution(); if ($access_log) { if ($request->getUser() && $request->getUser()->getPHID()) { $access_log->setData( array( 'u' => $request->getUser()->getUserName(), )); } } if (!$response) { $controller->willProcessRequest($uri_data); $response = $controller->processRequest(); } } catch (AphrontRedirectException $ex) { $response = id(new AphrontRedirectResponse()) ->setURI($ex->getURI()); } catch (Exception $ex) { $original_exception = $ex; $response = $application->handleException($ex); } try { $response = $application->willSendResponse($response); $response->setRequest($request); $response_string = $response->buildResponseString(); } catch (Exception $ex) { $write_guard->dispose(); if ($access_log) { $access_log->write(); } if ($original_exception) { $ex = new PhutilAggregateException( "Multiple exceptions during processing and rendering.", array( $original_exception, $ex, )); } phabricator_fatal('[Rendering Exception] '.$ex->getMessage()); } $write_guard->dispose(); // TODO: Share the $sink->writeResponse() pathway here? $sink = new AphrontPHPHTTPSink(); $sink->writeHTTPStatus($response->getHTTPResponseCode()); $headers = $response->getCacheHeaders(); $headers = array_merge($headers, $response->getHeaders()); $sink->writeHeaders($headers); // TODO: This shouldn't be possible in a production-configured environment. if (DarkConsoleXHProfPluginAPI::isProfilerRequested() && DarkConsoleXHProfPluginAPI::isProfilerRequested() === 'all') { $profile = DarkConsoleXHProfPluginAPI::stopProfiler(); $profile = '
'. ''. '>>> View Profile <<<'. ''. '
'; if (strpos($response_string, '') !== false) { $response_string = str_replace( '', ''.$profile, $response_string); } else { $sink->writeData($profile); } } $sink->writeData($response_string); if ($access_log) { $access_log->setData( array( 'c' => $response->getHTTPResponseCode(), 'T' => (int)(1000000 * (microtime(true) - $__start__)), )); $access_log->write(); } /** * @group aphront */ function setup_aphront_basics() { $aphront_root = dirname(dirname(__FILE__)); $libraries_root = dirname($aphront_root); $root = null; if (!empty($_SERVER['PHUTIL_LIBRARY_ROOT'])) { $root = $_SERVER['PHUTIL_LIBRARY_ROOT']; } ini_set( 'include_path', $libraries_root.PATH_SEPARATOR.ini_get('include_path')); @include_once $root.'libphutil/src/__phutil_library_init__.php'; if (!@constant('__LIBPHUTIL__')) { echo "ERROR: Unable to load libphutil. Put libphutil/ next to ". "phabricator/, or update your PHP 'include_path' to include ". "the parent directory of libphutil/.\n"; exit(1); } // Load Phabricator itself using the absolute path, so we never end up doing // anything surprising (loading index.php and libraries from different // directories). phutil_load_library($aphront_root.'/src'); phutil_load_library('arcanist/src'); } function phabricator_fatal_config_error($msg) { phabricator_fatal("CONFIG ERROR: ".$msg."\n"); } function phabricator_detect_bad_base_uri() { $conf = PhabricatorEnv::getEnvConfig('phabricator.base-uri'); $uri = new PhutilURI($conf); switch ($uri->getProtocol()) { case 'http': case 'https': break; default: return phabricator_fatal_config_error( "'phabricator.base-uri' is set to '{$conf}', which is invalid. ". "The URI must start with 'http://' or 'https://'."); } if (strpos($uri->getDomain(), '.') === false) { phabricator_fatal_config_error( "'phabricator.base-uri' is set to '{$conf}', which is invalid. The URI ". "must contain a dot ('.'), like 'http://example.com/', not just ". "'http://example/'. Some web browsers will not set cookies on domains ". "with no TLD, and Phabricator requires cookies for login. ". "If you are using localhost, create an entry in the hosts file like ". "'127.0.0.1 example.com', and access the localhost with ". "'http://example.com/'."); } } function phabricator_detect_insane_memory_limit() { $memory_limit = ini_get('memory_limit'); $char_limit = 12; if (strlen($memory_limit) <= $char_limit) { return; } // colmdoyle ran into an issue on an Ubuntu box with Suhosin where his // 'memory_limit' was set to: // // 3232323232323232323232323232323232323232323232323232323232323232M // // Not a typo. A wizard did it. // // Anyway, with this 'memory_limit', the machine would immediately fatal // when executing the ini_set() later. I wasn't able to reproduce this on my // EC2 Ubuntu + Suhosin box, but verified that it caused the problem on his // machine and that setting it to a more sensible value fixed it. Since I // have no idea how to actually trigger the issue, we look for a coarse // approximation of it (a memory_limit setting more than 12 characters in // length). phabricator_fatal_config_error( "Your PHP 'memory_limit' is set to something ridiculous ". "(\"{$memory_limit}\"). Set it to a more reasonable value (it must be no ". "more than {$char_limit} characters long)."); } function phabricator_shutdown() { $event = error_get_last(); if (!$event) { return; } if ($event['type'] != E_ERROR && $event['type'] != E_PARSE) { return; } $msg = ">>> UNRECOVERABLE FATAL ERROR <<<\n\n"; if ($event) { // Even though we should be emitting this as text-plain, escape things just // to be sure since we can't really be sure what the program state is when // we get here. $msg .= phutil_escape_html($event['message'])."\n\n"; $msg .= phutil_escape_html($event['file'].':'.$event['line']); } // flip dem tables $msg .= "\n\n\n"; $msg .= "\xe2\x94\xbb\xe2\x94\x81\xe2\x94\xbb\x20\xef\xb8\xb5\x20\xc2\xaf". "\x5c\x5f\x28\xe3\x83\x84\x29\x5f\x2f\xc2\xaf\x20\xef\xb8\xb5\x20". "\xe2\x94\xbb\xe2\x94\x81\xe2\x94\xbb"; phabricator_fatal($msg); } function phabricator_fatal($msg) { global $access_log; if ($access_log) { $access_log->setData( array( 'c' => 500, )); $access_log->write(); } header( 'Content-Type: text/plain; charset=utf-8', $replace = true, $http_error = 500); error_log($msg); echo $msg; exit(1); }