@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
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Don't 302 to an external URI, even after CSRF POST

Summary:
Via HackerOne. This defuses an attack which allows users to steal OAuth tokens through a clever sequence of steps:

- The attacker begins the OAuth workflow and copies the Facebook URL.
- The attacker mutates the URL to use the JS/anchor workflow, and to redirect to `/phame/live/X/` instead of `/login/facebook:facebook.com/`, where `X` is the ID of some blog they control. Facebook isn't strict about paths, so this is allowed.
- The blog has an external domain set (`blog.evil.com`), and the attacker controls that domain.
- The user gets stopped on the "live" controller with credentials in the page anchor (`#access_token=...`) and a message ("This blog has moved...") in a dialog. They click "Continue", which POSTs a CSRF token.
- When a user POSTs a `<form />` with no `action` attribute, the browser retains the page anchor. So visiting `/phame/live/8/#anchor` and clicking the "Continue" button POSTs you to a page with `#anchor` intact.
- Some browsers (including Firefox and Chrome) retain the anchor after a 302 redirect.
- The OAuth credentials are thus preserved when the user reaches `blog.evil.com`, and the attacker's site can read them.

This 302'ing after CSRF post is unusual in Phabricator and unique to Phame. It's not necessary -- instead, just use normal links, which drop anchors.

I'm going to pursue further steps to mitigate this class of attack more thoroughly:

- Ideally, we should render forms with an explicit `action` attribute, but this might be a lot of work. I might render them with `#` if no action is provided. We never expect anchors to survive POST, and it's surprising to me that they do.
- I'm going to blacklist OAuth parameters (like `access_token`) from appearing in GET on all pages except whitelisted pages (login pages). Although it's not important here, I think these could be captured from referrers in some cases. See also T4342.

Test Plan: Browsed all the affected Phame interfaces.

Reviewers: btrahan

Reviewed By: btrahan

CC: aran, arice

Differential Revision: https://secure.phabricator.com/D8481

+45 -32
+27 -24
src/applications/phame/controller/blog/PhameBlogLiveController.php
··· 30 30 } 31 31 32 32 if ($blog->getDomain() && ($request->getHost() != $blog->getDomain())) { 33 - $base_uri = 'http://'.$blog->getDomain().'/'; 34 - if ($request->isFormPost()) { 35 - return id(new AphrontRedirectResponse()) 36 - ->setURI($base_uri.$this->more); 37 - } else { 38 - // If we don't have CSRF, return a dialog instead of automatically 39 - // redirecting, to prevent this endpoint from serving semi-open 40 - // redirects. 41 - $dialog = id(new AphrontDialogView()) 42 - ->setTitle(pht('Blog Moved')) 43 - ->setUser($user) 44 - ->appendChild( 45 - pht('This blog is now hosted at %s.', 46 - $base_uri)) 47 - ->addSubmitButton(pht('Continue')); 48 - return id(new AphrontDialogResponse())->setDialog($dialog); 49 - } 33 + $base_uri = $blog->getLiveURI(); 34 + 35 + // Don't redirect directly, since the domain is user-controlled and there 36 + // are a bevy of security issues associated with automatic redirects to 37 + // external domains. 38 + 39 + // Previously we CSRF'd this and someone found a way to pass OAuth 40 + // information through it using anchors. Just make users click a normal 41 + // link so that this is no more dangerous than any other external link 42 + // on the site. 43 + 44 + $dialog = id(new AphrontDialogView()) 45 + ->setTitle(pht('Blog Moved')) 46 + ->setUser($user) 47 + ->appendParagraph(pht('This blog is now hosted here:')) 48 + ->appendParagraph( 49 + phutil_tag( 50 + 'a', 51 + array( 52 + 'href' => $base_uri, 53 + ), 54 + $base_uri)) 55 + ->addCancelButton('/'); 56 + 57 + return id(new AphrontDialogResponse())->setDialog($dialog); 50 58 } 51 59 52 60 $phame_request = clone $request; 53 61 $phame_request->setPath('/'.ltrim($this->more, '/')); 54 62 55 - if ($blog->getDomain()) { 56 - $uri = new PhutilURI('http://'.$blog->getDomain().'/'); 57 - } else { 58 - $uri = '/phame/live/'.$blog->getID().'/'; 59 - $uri = PhabricatorEnv::getURI($uri); 60 - } 63 + $uri = $blog->getLiveURI(); 61 64 62 65 $skin = $blog->getSkinRenderer($phame_request); 63 66 $skin 64 67 ->setBlog($blog) 65 - ->setBaseURI((string)$uri); 68 + ->setBaseURI($uri); 66 69 67 70 $skin->willProcessRequest(array()); 68 71 return $skin->processRequest();
+1 -4
src/applications/phame/controller/blog/PhameBlogViewController.php
··· 158 158 $blog, 159 159 PhabricatorPolicyCapability::CAN_JOIN); 160 160 161 - $must_use_form = $blog->getDomain(); 162 - 163 161 $actions->addAction( 164 162 id(new PhabricatorActionView()) 165 163 ->setIcon('new') ··· 172 170 id(new PhabricatorActionView()) 173 171 ->setUser($user) 174 172 ->setIcon('world') 175 - ->setHref($this->getApplicationURI('live/'.$blog->getID().'/')) 176 - ->setRenderAsForm($must_use_form) 173 + ->setHref($blog->getLiveURI()) 177 174 ->setName(pht('View Live'))); 178 175 179 176 $actions->addAction(
+2 -4
src/applications/phame/controller/post/PhamePostViewController.php
··· 141 141 142 142 $blog = $post->getBlog(); 143 143 $can_view_live = $blog && !$post->isDraft(); 144 - $must_use_form = $blog && $blog->getDomain(); 145 144 146 145 if ($can_view_live) { 147 - $live_uri = 'live/'.$blog->getID().'/post/'.$post->getPhameTitle(); 146 + $live_uri = $blog->getLiveURI($post); 148 147 } else { 149 148 $live_uri = 'post/notlive/'.$post->getID().'/'; 149 + $live_uri = $this->getApplicationURI($live_uri); 150 150 } 151 - $live_uri = $this->getApplicationURI($live_uri); 152 151 153 152 $actions->addAction( 154 153 id(new PhabricatorActionView()) ··· 156 155 ->setIcon('world') 157 156 ->setHref($live_uri) 158 157 ->setName(pht('View Live')) 159 - ->setRenderAsForm($must_use_form) 160 158 ->setDisabled(!$can_view_live) 161 159 ->setWorkflow(!$can_view_live)); 162 160
+15
src/applications/phame/storage/PhameBlog.php
··· 136 136 return self::$requestBlog; 137 137 } 138 138 139 + public function getLiveURI(PhamePost $post = null) { 140 + if ($this->getDomain()) { 141 + $base = new PhutilURI('http://'.$this->getDomain().'/'); 142 + } else { 143 + $base = '/phame/live/'.$this->getID().'/'; 144 + $base = PhabricatorEnv::getURI($base); 145 + } 146 + 147 + if ($post) { 148 + $base .= '/post/'.$post->getPhameTitle(); 149 + } 150 + 151 + return $base; 152 + } 153 + 139 154 140 155 /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ 141 156