The Cyber Archive

Hidden Chains: Revealing High-Impact Bugs from Bounty Submissions

Learn how Snapchat uncovered three chained, high-impact bug bounty findings—supply chain RCE, Android deep link abuse, and Jupyter XSS-to-RCE—and the program capabilities each forced them to build.


Vinay & Murali presenting talk - Hidden Chains: Revealing High-Impact Bugs from Bounty submissions at OWASP Global AppSec USA 2024
Vinay & Murali presenting talk - Hidden Chains: Revealing High-Impact Bugs from Bounty submissions at OWASP Global AppSec USA 2024

A transitive Node.js dependency was silently serving a malicious binary from an abandoned S3 bucket — no CVE existed, no alert fired, and a researcher found it first. Bug bounty program security at scale means catching exactly these kinds of supply chain, cross-feature, and privilege-escalation chains that automated tools miss and most triage processes underestimate.

This post dissects three high-severity findings from Snapchat’s HackerOne[2] program: an FSEvents supply chain compromise, an Android deep link enabling silent video call initiation, and a blind XSS that escalated to full remote code execution through a Jupyter notebook’s[5] IPython kernel.

Key Takeaways

  • You'll learn how to identify and respond to supply chain attacks involving transitive open-source dependencies and abandoned cloud storage buckets, even when no CVE exists.
  • You'll be able to recognize cross-feature interaction vulnerabilities—such as unintended Android deep link abuse—that emerge when new platform features expose internal APIs not designed for external access.
  • Apply systematic root cause analysis and severity re-evaluation to bug bounty reports: a researcher-rated "low" XSS can be a critical RCE when Jupyter notebook IPython kernels are in scope.

Software Supply Chain Security: Open Source Dependencies and S3 Bucket Takeover

FSEvents supply chain attack flow via abandoned AWS S3 bucket

Software supply chain vulnerability doesn’t always arrive through a compromised maintainer account or a deliberate backdoor. Sometimes the attack surface is structural—baked into how a package was architected years before anyone thought to abuse it. The FSEvents[1] incident at Snapchat is a textbook example of a transitive dependency risk that bypassed every standard detection control.

FSEvents is a third-party Node.js library for file system monitoring. On its own, most engineers have never heard of it. Yet it sees over 19 million weekly downloads—not because developers intentionally install it, but because it sits at the bottom of a common dependency chain:

nodemon → chokidar → FSEvents

Any Node.js developer using nodemon[3] for development file watching inherits FSEvents automatically via chokidar[4]. This transitive dependency pattern makes the attack surface enormous and largely invisible to the engineers it affects.

The Vulnerability: Build-Time Binary Download from S3

In older versions of FSEvents, the build process included a step that downloaded a pre-compiled binary from an AWS S3 bucket at install/build time and executed it. This is already a significant red flag—an npm package reaching out to external cloud storage to pull an executable during build is an implicit trust decision most developers never consciously made.

In April of the prior year, a GitHub Advisory Database[8] entry was published documenting a security vulnerability in FSEvents that could lead to remote code execution (RCE). The vulnerability was patched and the FSEvents developer deleted the S3 bucket. However, the disclosure had a critical blind spot: no CVE was assigned.

This single omission meant that every standard open-source vulnerability scanner—tools that rely on CVE databases as their source of truth—had no record of the issue. The advisory existed; the fix existed; but the detection pathway most organizations depend on was completely dark.

The S3 Namespace Window: AWS’s 3-Month Reclaim Delay

AWS S3 has a documented behavior that became the linchpin of this incident: when a bucket is deleted, its namespace is not immediately released. The name remains reserved for up to three months, during which it cannot be claimed by anyone. After that window closes, the name becomes available again on a first-come, first-served basis.

The FSEvents developer deleted the bucket after patching. Three months later, the namespace opened up. A security researcher claimed it and began serving a different binary—a proof-of-concept payload that collected basic execution environment data: the present working directory, machine name, and similar metadata. The binary was not intentionally destructive, but the mechanism was live RCE: arbitrary code executing on developer machines during a routine npm install.

Snapchat received the bug bounty report from HackerOne[2] stating that a vulnerable version of FSEvents had executed in their environment, with proof of execution. This immediately triggered a security incident.

Incident Response: What Snapchat Did

The detection and response team conducted a complete payload analysis, confirming:

  • Execution was limited to macOS (where the binary targeted the platform)
  • The binary’s behavior was PoC-level—basic environment enumeration only
  • No malicious impact was observed from this specific instance

Snapchat then performed a full dependency scan across all services to find every instance of the vulnerable FSEvents version in their environment—not just the two or three the researcher had identified. They also leveraged endpoint detection software to scan developer laptops for evidence of vulnerable package downloads.

A key insight from triage: the same researcher was submitting this issue across multiple HackerOne programs simultaneously, because the root cause was universal. Snapchat escalated to AWS directly, which resulted in the FSEvents bucket namespace being added to AWS’s reserved list, permanently blocking any future reclaim of that specific name.

The Capability Gaps This Incident Exposed

1. Dependency scanners that only track CVEs miss advisory-only vulnerabilities. The GitHub Advisory Database[8] entry existed. The fix existed. But without a CVE, automated scanning produced zero alerts.

2. Service inventory debt compounds mean time to remediate (MTTR). Once the vulnerability was confirmed, Snapchat needed to route the finding to service owners across dozens of services. Their service inventory had accumulated drift—people had left, moved teams, records hadn’t been updated. At 250+ employees, a live service inventory with named owners is not optional.

PoC: FSEvents S3 Bucket Takeover — Supply Chain RCE via Abandoned AWS Namespace

A security researcher exploited AWS S3’s bucket namespace reclaim window to serve a malicious binary to Node.js developers via the FSEvents transitive dependency, achieving code execution on developer machines during routine npm builds—bypassing all CVE-based scanners because the underlying GitHub advisory had no assigned CVE.

Proof of Concept Steps:

  1. Identify the target dependency: The nodemon → chokidar → FSEvents chain means FSEvents is present on virtually every Node.js developer’s machine without being a direct install.
  2. Locate the vulnerable build behavior: In older versions, FSEvents’ postinstall hook downloads a pre-compiled binary from a hardcoded AWS S3 URL and executes it. No additional user interaction required beyond npm install.
  3. Wait for the advisory window: The GitHub advisory is published, developer patches and deletes the S3 bucket. No CVE is assigned — automated scanners see nothing.
  4. Exploit the 90-day namespace delay: After AWS releases the bucket name, register a new S3 bucket using the exact same name as the deleted FSEvents bucket.
  5. Host a substitute binary: Upload a custom binary to the reclaimed bucket. It is served at the same URL the vulnerable FSEvents versions expect.
  6. Trigger execution via npm install: Any developer or CI/CD pipeline running a vulnerable FSEvents version automatically fetches and executes the substitute binary. macOS-targeted; no user interaction beyond a standard install command.
  7. Confirm execution via callback: The PoC binary transmits collected environment data, providing proof of execution for the bug bounty report.

PoC Rating: The researcher’s payload was a benign environment-enumeration binary. A weaponized variant—with C2 callbacks, credential harvesting, or a reverse shell—would follow the same delivery mechanism but is not documented in the transcript.

Actionable Takeaways

  • Supplement CVE-based dependency scanners with GitHub Advisory Database (GHSA) monitoring. Tools like Dependabot,[9] OSV-Scanner,[10] or Grype[11] can track advisories that have not yet received a CVE assignment, closing the detection gap that allowed this vulnerability to persist undetected.
  • Audit your Node.js dependency trees for build-time network calls. Any npm package that downloads executables or scripts from external URLs during install or build (via postinstall hooks) should be treated as a supply chain risk and reviewed for the trustworthiness of the remote endpoint.
  • Invest in and maintain a live service inventory with named owners. When a critical vulnerability requires immediate patching across multiple services, the bottleneck is almost always routing—finding who owns which service. A stale inventory makes MTTR worse; a current one makes incident response significantly faster.

Common Pitfalls

  • Relying exclusively on CVE databases for open-source vulnerability detection. The FSEvents advisory predated its CVE assignment, meaning all standard scanners returned clean results while a live RCE mechanism was available on developer machines. Advisory-only vulnerabilities are a real and growing class of risk.
  • Assuming that because a patch was issued, the attack surface is closed. In this case, the fix was deployed, the S3 bucket was deleted, and the issue was considered resolved—yet the AWS namespace reclaim window created a secondary attack window three months later. Patching the package is necessary but not sufficient; verify that all downstream infrastructure dependencies are also secured.

To understand this Android deep link exploit, you need a clear mental model of how Android applications expose functionality externally. Every Android application ships with an AndroidManifest.xml—a declarative XML document defining the application’s permissions, capabilities, and behavioral contracts. Three constructs are relevant here:

  • Activities: Each screen in an Android app is an activity (profile screen, login screen, calling interface).
  • Intents and intent filters: Activities communicate through intents—structured messages with action types and data payloads. Intent filters declare which intents an activity handles.
  • Deep links: Defined within the manifest via URI schemes and intent filters, deep links let external applications or browsers invoke specific activities directly. A URI like snapchat://... is resolved by Android against registered handlers and routed into the corresponding activity.

Deep links enable necessary cross-app functionality, but they represent a trust boundary: anything that can craft the right URI can trigger application behavior directly, bypassing any UI flow that would normally gate that action.

Snapchat received a bug bounty report that came in as a screen recording. The researcher demonstrated the latest version of Snapchat installed, opened a web browser, clicked a link—and immediately the Snapchat app launched and began initiating a video call in the background.

Attack precondition: The attacker must have an existing Snapchat friend relationship with the victim.

Full attack chain:

  1. Attacker initiates a conversation via the Snapchat Web client (introduced in 2022).
  2. Attacker captures the conversation ID exposed by the web client—an identifier not previously surfaced through the mobile app.
  3. Attacker crafts a snapchat:// deep link URI with: scheme snapchat://, action start call, media type video, conversation ID (captured), call type one-to-one.
  4. Attacker delivers the link to the victim via any channel—email, forum post, SMS.
  5. Victim clicks the link. Android resolves the scheme to Snapchat, which processes the deep link and immediately begins initiating the video call—with no confirmation prompt.

The victim’s camera and microphone could be activated entirely from a link click they had no reason to associate with a call being started.

Root cause: The deep link handler accepted the conversation ID and call parameters as sufficient authorization to initiate a call. No re-verification of user intent was performed.

The Cross-Feature Interaction That Made It Possible

The conversation ID was never exposed through the Snapchat mobile app. When Snapchat introduced Snapchat Web in 2022, the web client naturally surfaced the conversation ID as part of its URL structure—normal web architecture.

What the team had not modeled: how a feature designed for the web client would interact with the mobile application’s existing deep link surface. The conversation ID, previously internal, was now accessible to anyone with Snapchat Web open. And the mobile deep link handler accepted it as input to initiate a call.

Neither component was broken in isolation. The interaction between them was the vulnerability.

By combining the conversation ID exposed by Snapchat Web with the Android app’s deep link handler, an attacker who is friends with a victim can deliver a crafted snapchat:// URI that silently initiates a full video call—activating camera and microphone without any confirmation prompt—entirely from a link click.

Proof of Concept Steps:

  1. Establish friendship with the target — required by the friend graph model.
  2. Open Snapchat Web and initiate a conversation with the target. Observe the conversation ID in the URL or API response.
  3. Capture the conversation ID — a unique identifier for the one-to-one conversation.
  4. Craft the deep link URI:
    • Scheme: snapchat://
    • Action: new chat / call initiation
    • Media type: video
    • Conversation ID: captured value
    • Call type: one-to-one
  5. Deliver the URI via any out-of-band channel (email, forum post, SMS). No Snapchat session required.
  6. Victim clicks the link. Android routes the snapchat:// URI to the app.
  7. Video call initiates without confirmation. Camera and microphone activate. All conversation participants receive the incoming call.

Root Cause: The deep link handler treated URI presence as user intent. The fix added an explicit confirmation dialog before activating call media from a deep link.

Incident Response and the Privacy Bug Bar

Snapchat classified this as a privacy incident—not merely a security bug. The unauthorized video initiation implicated user privacy directly, triggering a different stakeholder set, severity criteria, and response timeline.

The team deployed monitoring while the patch propagated through the Android Play Store. The monitoring strategy was elegant: calls initiated via the snapchat:// URI scheme are anomalous—direct in-app calls use a different code path entirely. Watching for call-initiation events sourced from the URI intent handler allowed detection of any abuse during the patch adoption window. No malicious exploitation in the wild was identified.

Privacy bug bar factors Snapchat found useful:

  • Data sensitivity: User-generated content vs. aggregated content vs. raw metadata carry different severity implications.
  • Exposure scale: Bug affecting 0.1% of users (A/B test cohort) vs. 50% of all users.
  • Friend graph constraint: Exploitable only within a friend relationship = lower severity than arbitrary targeting.
  • Likelihood of exploitation: Standard exploitability factors apply equally to privacy bugs.

Actionable Takeaways

  • Model cross-feature interactions whenever you introduce a new surface (web client, public API, third-party integration). Document which internal identifiers or state each new feature exposes and audit whether those identifiers are accepted as input by any existing deep link handlers, intent filters, or API endpoints that could trigger sensitive actions without re-authorization.
  • Require explicit user confirmation for any action initiated via deep link that triggers a sensitive capability (camera, microphone, location, payments). The deep link mechanism should navigate the user to the relevant UI, not bypass it. A confirmation step breaks the silent execution chain that made this attack possible.
  • Establish a privacy bug bar separate from your security bug bar, defined in collaboration with legal and privacy teams. Include severity factors specific to privacy (data sensitivity tiers, affected user percentage, friend graph constraints) so that privacy-implicating bugs are triaged and routed correctly.

Common Pitfalls

  • Treating feature launches as isolated events. The Snapchat Web launch was functionally correct—but nobody modeled how the conversation IDs it exposed would interact with the mobile deep link surface. Any time a new feature adds externally visible identifiers or tokens, trigger a review of every place those identifiers could be consumed by existing application logic.
  • Delaying monitoring setup until after a patch is fully deployed. Since app updates are not instantaneous—users may run unpatched versions for days or weeks—monitoring for exploitation during the patch propagation window is critical. Without telemetry on the URI intent call path, Snapchat would have had no visibility into whether the vulnerability was being actively abused.

XSS Escalation to Remote Code Execution in Jupyter Notebook Environments

XSS to RCE attack pipeline through BigQuery and Jupyter notebook IPython kernel

One useful mental model for Jupyter notebooks[5] is a spreadsheet on a browser with a database in the back end. But this analogy breaks down in one critical way: a spreadsheet recalculates formulas in a sandboxed local process. Jupyter notebooks execute Python code inside an IPython kernel—a persistent, full-capability Python runtime that runs on the host machine with access to the local file system, environment variables, and network stack.

Jupyter supports three hosting models:

  • Local: The IPython kernel runs directly on the data scientist’s laptop or workstation.
  • Private hosted: An internal infrastructure team runs a shared Jupyter server.
  • Cloud hosted: Managed Jupyter environments (Google Colab, AWS SageMaker, Databricks).

In a local deployment—which remains common in data science organizations—a successful XSS that reaches a Jupyter notebook means code executes on the analyst’s workstation.

Jupyter’s Security Model and Its Critical Exception

The Jupyter development team built the security model around a clear principle: did the user initiate an action that resulted in code being executed? From this:

  • Untrusted HTML or JavaScript is not rendered or executed.
  • JavaScript or HTML in Markdown cells is not executed.
  • No code cell executes until the user clicks run.

These controls are reasonable and, in normal operation, effective.

But one significant exception exists: output generated from code execution is considered trusted, because the user initiated the execution that produced it. If attacker-controlled content reaches a cell’s output as the result of executing a query, it inherits the trusted status of that output context.

This is what made the Snapchat incident possible.

The Attack Chain: User Agent → BigQuery → Jupyter → IPython Kernel

The bug bounty report arrived as a blind XSS finding, rated low severity. The researcher had injected an XSS polyglot into the User-Agent header across multiple Snapchat endpoints. An XSS polyglot is a string crafted to execute as JavaScript across multiple injection contexts (HTML body, HTML attribute, JavaScript string) without modification—the same string works wherever the data flows.

The researcher waited two weeks before receiving a callback—a DNS or HTTP ping-back from a server they controlled, confirming the injected string had executed somewhere in Snapchat’s environment. The report was filed as low severity because the researcher didn’t know where execution had occurred.

Snapchat’s investigation revealed the full chain:

  1. XSS polyglot injected in the HTTP User-Agent string during normal traffic.
  2. Snapchat backend captures User-Agent strings in standard application logs.
  3. Logs flow into BigQuery[6] as part of routine analytics pipelines.
  4. A data scientist queries BigQuery log data in a Jupyter notebook to run a report.
  5. The User-Agent field (containing the polyglot) is rendered in the notebook’s output cell.
  6. Because the output came from a user-initiated code execution, it is treated as trusted.
  7. The XSS polyglot executes in the Jupyter browser context.

In a standard web application, this would be a serious but contained XSS. In Jupyter, it was the beginning of the escalation.

XSS to RCE: The IPython Kernel Bridge

When JavaScript executes inside a Jupyter notebook’s browser context, it is not isolated from the kernel. The Jupyter frontend communicates with the IPython kernel over a WebSocket-based messaging protocol. JavaScript running in the browser context can send messages directly to the IPython kernel—requesting it to execute arbitrary Python code.

A sufficiently crafted injection payload can:

  1. Execute JavaScript in the browser context (standard XSS).
  2. From that JavaScript context, interact with the Jupyter kernel API.
  3. Issue Python commands to the kernel—os.system(), subprocess.run(), or any shell execution primitive.
  4. The kernel executes those commands on the host machine with the privileges of the user running the Jupyter process.

The Snapchat team demonstrated this live: a crafted XSS string caused the Jupyter frontend to display a standard JavaScript alert (classic XSS confirmation), then silently launched calc.exe on the demo machine through the IPython kernel bridge.

PoC: Blind XSS to RCE — Jupyter Notebook IPython Kernel Exploitation via User Agent Injection

A researcher injected an XSS polyglot into the HTTP User-Agent header; the payload traversed the logging pipeline into BigQuery and was rendered unsanitized in a Jupyter notebook output, where it escalated from browser-context XSS to full RCE by interacting with the IPython kernel—executing arbitrary system commands on the data scientist’s machine with no additional user interaction.

Proof of Concept Steps:

  1. Craft an XSS polyglot — a string that executes across HTML body, attribute, and JavaScript string contexts without modification. Inject it as the User-Agent header on HTTP requests to Snapchat endpoints.
  2. Inject via User-Agent — logging systems capture this header verbatim, bypassing frontend input validation.
  3. Wait for asynchronous execution — the blind XSS payload includes a callback mechanism (DNS/HTTP ping-back). The researcher waited two weeks before receiving confirmation.
  4. Receive the callback — confirms execution somewhere in the environment. Filed as low-severity because location is unknown.
  5. Trace the injection path (Snapchat’s investigation):
    • User-Agent → application logs → BigQuery data warehouse
    • Data scientist queries BigQuery → renders results in Jupyter notebook
    • User-Agent column rendered as trusted HTML output in notebook cell
    • XSS polyglot executes in browser context
  6. Escalate to RCE via IPython kernel:
    • JavaScript executes in Jupyter browser context
    • Uses Jupyter kernel messaging API (WebSocket) to send Python execution request
    • IPython kernel executes os.system() or equivalent — RCE on host machine
  7. Live demo confirmation: Injected XSS string → JavaScript alert box → calc.exe launched on demo laptop via IPython kernel bridge.
  8. Production impact: Analyst’s machine had access to internal infrastructure, credentials, and Snapchat’s internal network. Snapchat increased the bounty payout to reflect the actual severity.

Fix: Contextual HTML encoding applied to all user-controlled fields before rendering in Jupyter notebook output cells.

Hardening Jupyter Notebook Environments

1. Contextually encode all data rendered in notebook outputs. Any field from user-controlled input—HTTP headers, URL parameters, free-text database columns—must be HTML-encoded before rendering in a notebook output cell.

2. Patch Jupyter versions regularly. These updates address kernel API exposure, Content Security Policies, and cross-origin restrictions. They are frequently deprioritized because Jupyter is assumed to be in a “trusted context”—but as this incident demonstrates, the trust boundary can be violated by data flowing from external sources.

3. Prefer cloud-hosted Jupyter over local installations. Cloud-hosted kernels (Google Colab, SageMaker, Databricks) confine RCE to a managed environment rather than the analyst’s workstation.

4. Never accept a researcher’s severity rating as final. Without internal knowledge of where injected data ends up, a researcher can only assess what they can observe. Severity must be reassessed with full context: which internal systems process this data, in what tooling, at what privilege level.

Actionable Takeaways

  • Audit all data pipelines that feed into Jupyter notebooks for user-controlled fields. Any column in a BigQuery table, log aggregator, or data warehouse that originates from HTTP headers, URL parameters, or user-generated content must be flagged and treated as untrusted. Apply HTML encoding before rendering in notebook outputs, and consider automated checks in CI/CD pipelines that flag new data queries pulling unencoded user-controlled fields into notebook contexts.
  • Re-evaluate the severity of any XSS finding that involves internal tooling backed by a code execution engine. Jupyter notebooks, RStudio, Observable, and similar environments all have kernel or execution backends that an XSS payload can interact with. A "low severity" blind XSS becomes critical the moment you trace where the injected data ends up in your internal toolchain.
  • Migrate local Jupyter deployments to cloud-hosted managed environments. Local kernel execution means RCE lands directly on an analyst's machine with full access to local credentials, network shares, and corporate VPN. Cloud-hosted kernels constrain the blast radius. If migration isn't immediately feasible, enforce strict Content Security Policies (CSP) on self-hosted Jupyter instances.

Common Pitfalls

  • Assuming internal analytics tools are outside the XSS threat model. The data flowing into Jupyter notebooks passes through multiple systems—HTTP logs, data warehouses, ETL pipelines—any of which may carry user-controlled content from external sources. "This is an internal tool" is not a security boundary; it is an observation about who uses the tool, not about what data it processes.
  • Treating notebook output as trusted simply because a user executed the query. Jupyter's security model grants trusted status to execution outputs because the assumption is that the user controls what code they run. But when external data containing injected payloads is the output of that execution, the trust grant is misapplied.

Building and Operationalizing a Mature Bug Bounty Program

Before getting into how to run a bug bounty program, the first thing to internalize is what it is not. A bug bounty is not a substitute for your SDLC security controls, threat modeling, or penetration testing. Organizations that treat a bug bounty as a pentest-as-a-service are setting themselves up for failure. The shift-left principle still applies—bug bounties are a complementary layer, not a replacement for foundational security engineering.

Snapchat’s program offers a useful reference point. Launched in 2015, it now handles close to 1,000 submissions per year. Payouts reach $35,000 for critical findings on the main Snapchat application and up to $5,000 for lower-tier assets. As of 2024, the program has paid out over $1 million total.

Launch Private First

The most important structural decision when launching a bug bounty is starting private. Launching public immediately is, in the Snapchat team’s assessment, “a disaster.” Here’s why:

  • Researchers in a public program immediately chase low-hanging fruit: misconfigured assets, outdated software versions, known CVEs. You receive a flood of high-volume, low-signal submissions.
  • If your triage capacity can’t handle the volume, the backlog grows, response times increase, and researcher experience degrades.
  • Every bug bounty launch surfaces unexpected scope. A private launch gives you time to discover and address it in a controlled environment.

Run a private program for 6 months to a year before going public. Start small on scope and budget—add scope incrementally as you build triage capacity.

Operational Capabilities You Must Have

Risk-based SLAs: Every vulnerability category needs a defined remediation timeline. Without SLAs, triage decisions are ad hoc and fix timelines are unpredictable.

Service inventory with named owners: When a critical finding comes in, the first question is: who owns the affected service? The FSEvents incident exposed this gap directly. At 250+ employees, a live, maintained service inventory is a prerequisite for effective incident response—not a nice-to-have.

Reachability analysis: A researcher may identify a vulnerability in ten services. Prioritize internet-facing, user-accessible services over internal ones with limited exposure.

Completeness mindset: A researcher gives you one instance of a problem. Your job is to find all instances using your source code and internal knowledge. The Jupyter XSS incident reinforced variant analysis—auditing all .ipynb files across the environment—as a required triage step.

Cross-functional collaboration infrastructure: Bug bounty findings span team boundaries. Pre-established escalation paths to legal, privacy, and incident response are materially better than building those relationships during an incident.

Resources for Getting Started

  • securitytemplates.com[7] (maintained by Robert Auger): Templates covering bug bounty policy, safe harbor language, scope definitions, and reporting guidance.
  • Bug Bounty Community of Interest: A Slack community with monthly meetings focused on operational challenges—scope, budget, researcher disputes, triage scaling.

Treat the Program as a Living System

Every incident processed through the bug bounty pipeline should be evaluated for capability gaps it exposed:

  • FSEvents → revealed a service inventory gap
  • Deep link → prompted a formalized privacy bug bar
  • Jupyter XSS → reinforced variant analysis as a standard triage step

A program that doesn’t evolve in response to what it encounters stops improving—and the attack surface keeps growing regardless.

Actionable Takeaways

  • Launch private for at least 6 months before going public. Use that window to calibrate triage capacity, refine scope and policy documentation, identify unexpected asset exposure, and build internal response workflows before submission volume scales up.
  • Before launching, audit your operational readiness against three prerequisites: a risk-based remediation SLA for each vulnerability class, a live service inventory with named owners, and documented escalation paths to legal, privacy, and incident response. These determine whether your MTTR is hours or weeks when a critical finding arrives.
  • After every significant incident, conduct a brief capability retrospective: what gap did this incident expose? What tool, process, or documentation was missing or inadequate? Fix it and update your program. The program should become more capable with each incident, not just process them.

Common Pitfalls

  • Treating a bug bounty as a pentest substitute. Bug bounty programs surface issues that evade your other controls—they are a detection layer, not a replacement for threat modeling, code review, or dedicated penetration testing.
  • Launching public immediately to maximize researcher participation. The volume and unpredictability of a public launch overwhelms triage teams that haven't scaled their processes, damages researcher experience through slow response times, and surfaces unexpected asset exposure in an uncontrolled way.

Bug Bounty Program Metrics, Researcher Engagement, and Continuous Improvement

Once a vulnerability management program has been running for 6 months to a year, the submission data it has accumulated becomes one of the most valuable signals you have about your security posture. The Snapchat team identified five specific metrics that drive meaningful program decisions:

1. Submissions by asset — Which assets are receiving researcher attention, and which are ignored? High submission counts indicate active targeting and researcher ROI. Low counts may indicate insufficient scope, poor documentation, or underpromotion. When an asset is underserved, temporarily increasing its payout nudges researchers toward it.

2. Submissions by vulnerability type — If submissions are dominated by a recurring class—XSS, IDOR, authentication bypasses—that’s a systemic engineering signal. The appropriate response is to build a developer-facing framework or automated detection rule that eliminates the class, not to keep patching individual instances.

3. Response efficiency (time-to-bounty) — The primary economic metric for researchers: how long from submission to payment? A slow-paying program drives experienced researchers to faster-paying alternatives. Internally, the equivalent is MTTR—from submission to fix deployment. Tracking MTTR over time reveals where bottlenecks are (triage lag, routing, engineering prioritization, deployment).

4. False positive and out-of-scope submission rate — A high rate is a symptom of unclear program documentation and a source of triage fatigue. Explicitly document known non-issues and maintain tight scope to reduce wasted effort on both sides.

5. Reward spend by asset and vulnerability type — Enables budget forecasting. If you haven’t invested in securing a particular asset or closing a vulnerability class, the reward spend data tells you what that investment is costing you reactively.

What Researchers Actually Care About

Direct feedback from Snapchat’s program participants:

Time to bounty is the north star metric. Administrative delays, ambiguous impact assessments, and slow triage all increase this metric and reduce researcher incentive to return.

Reduce friction for getting started. Provide premium subscriptions if your application requires them. Clear, accurate testing documentation helps researchers spend time on productive testing rather than onboarding friction.

Provide candid reasoning for payout decisions. When you adjust a bounty amount or change a severity classification, explain why. Opaque decisions breed distrust. Transparent communication builds long-term researcher relationships.

Treat researchers as professionals. The programs that consistently attract top researchers are those where investment is met with responsiveness, respect, and clear communication.

Preventing Stagnation: Gamification and Active Engagement

Targeted bonus campaigns: For a defined window (2 weeks to 1 month), increase payouts for a specific underserved asset without permanent scope or budget changes.

CTFs and time-limited challenges: Time-constrained competitions focused on specific stack components create structured incentives for deep, focused research.

Live hacking events: In-person events produce qualitatively different results. Researchers share context, chain findings collaboratively, and often combine two or three low-severity issues into a critical-severity chain that no individual researcher would have assembled alone.

Bug of the month: Select a well-reported finding monthly and publicize it with researcher consent. Most researchers want public recognition—this costs nothing but program operator time.

Leaderboards and small bonuses: Small discretionary bonuses—$100 on top of a $4,000 payout—produce outsized researcher satisfaction relative to their cost. Humans respond to recognition; make use of that.

Actionable Takeaways

  • Review your last 6 months of submissions and segment them by asset and vulnerability type. Use the asset distribution to identify underserved surfaces and consider targeted payout increases. Use the vulnerability type distribution to identify recurring classes and invest in systematic fixes—developer training, framework-level controls, or automated detection—rather than continuing to patch individual instances.
  • Measure and publish your time-to-bounty metric internally. Make it visible to program stakeholders the way engineering teams track deployment frequency or incident MTTR. If you don't measure it, you can't improve it, and it's the metric researchers use to decide whether your program is worth their time.
  • Run at least one live hacking event annually, focused on a specific high-value asset or feature area. The collaborative chain-finding dynamic of live events consistently surfaces findings that normal asynchronous program operations miss.

Common Pitfalls

  • Setting a scope and budget once at launch and never revisiting them. As your product surface expands, researcher skills evolve, and vulnerability classes shift, a static scope becomes stale. Quarterly scope and budget reviews—informed by submission metrics—keep the program calibrated to your actual attack surface.
  • Underestimating the compounding cost of slow triage and late payouts. A program that pays slowly loses experienced researchers to faster-paying programs. Once that reputation is established in the researcher community, it is slow to repair. Response efficiency is not just an operational metric—it's a reputation factor that directly affects the quality of your researcher pool.

Conclusion

The three incidents Vinay and Murali dissected from Snapchat’s bug bounty program share a structural pattern: a low-visibility root cause + an unexpected interaction + a trust assumption that proved incorrect. FSEvents assumed the S3 bucket endpoint would always be controlled by the original developer. The deep link handler assumed that URI presence equaled user intent. Jupyter assumed that execution outputs would never carry attacker-controlled content from external logging pipelines.

What made each finding impactful was not technical sophistication alone—it was the combination of a vulnerability with a context the researcher or operator hadn’t modeled. The lesson for security engineers is to invest in the detection, triage, and investigation infrastructure that surfaces these interactions before they become incidents: advisory-monitoring scanners, maintained service inventories, privacy bug bars, and systematic variant analysis.

For more on software supply chain security and defending against dependency-level attacks, see our coverage of related talks. For application security fundamentals including Android deep link defense and XSS mitigation, explore the broader topic hub. For bug bounty program management resources, the community and templates referenced in this talk are a practical starting point.


References & Tools

  1. FSEvents — Node.js file monitoring library; its build-time binary download from an AWS S3 bucket was the vector for the supply chain attack described in this article.
  2. HackerOne — Bug bounty platform used by Snapchat to manage vulnerability submissions, triage, and payouts; all findings discussed in this talk were submitted and disclosed through this platform.
  3. nodemon — Node.js development tool for file watching; introduces FSEvents as a transitive dependency via chokidar, propagating the supply chain vulnerability to virtually all Node.js developers using it.
  4. chokidar — Node.js cross-platform file watching library that sits between nodemon and FSEvents in the transitive dependency chain.
  5. Jupyter Notebooks — Interactive Python document environment used by Snapchat data scientists; its IPython kernel architecture elevated a standard XSS payload to full RCE by allowing injected JavaScript to send execution commands to the backend Python runtime.
  6. BigQuery — Google data warehouse where Snapchat stored application logs including HTTP user agent strings; the data pipeline from BigQuery to Jupyter was the channel through which the injected XSS polyglot reached the notebook rendering context.
  7. sectemplates — Resource by Robert Auger providing structured templates for bug bounty policies, safe harbor language, and scope definitions; recommended as a starting point for teams launching a new program.
  8. GitHub Advisory Database (GHSA) — Source of the FSEvents advisory; notable in this incident because no CVE was assigned, causing all CVE-based scanners to miss the vulnerability entirely.
  9. Dependabot — Automated dependency scanning tool that can track GitHub Security Advisories (GHSA) in addition to CVEs, closing the advisory-only vulnerability detection gap.
  10. OSV-Scanner — Open source vulnerability scanner by Google that queries the OSV database, which includes GHSA advisories not yet assigned CVEs.
  11. Grype — Container and filesystem vulnerability scanner that supports multiple advisory databases beyond NVD/CVE, including GHSA.
Frequently asked

Questions from the audience

What made the FSEvents supply chain attack undetectable by standard scanners?
No CVE was assigned to the GitHub advisory documenting the FSEvents vulnerability. Because standard open-source vulnerability scanners query CVE databases, the absence of a CVE meant all automated tools returned clean results even while a live RCE mechanism existed on developer machines.
How does XSS escalate to RCE in a Jupyter notebook environment?
Jupyter notebooks communicate with an IPython kernel via a WebSocket protocol. JavaScript executing in the Jupyter browser context can send messages directly to the kernel, requesting it to execute arbitrary Python code. This means injected JavaScript can break out of the browser sandbox and run OS-level commands on the host machine running the kernel.
What conditions are required to exploit the Snapchat Android deep link vulnerability?
The attacker must have an existing Snapchat friend relationship with the victim. Using the Snapchat Web client (introduced in 2022), the attacker captures a conversation ID, embeds it in a crafted snapchat:// URI, and delivers it to the victim. When the victim clicks the link, the Android deep link handler initiates a video call without presenting any confirmation prompt.
How long should a bug bounty program run privately before going public?
At least 6 months to a year. A private launch lets you calibrate triage capacity, refine scope and policy documentation, and discover unexpected asset exposure in a controlled environment before high-volume public submissions begin.
Watch on YouTube
Hidden Chains: Revealing High-Impact Bugs from Bounty Submissions
Vinay Prabhushankar, Murali Vadakke Puthanveetil, · 42 min
Watch talk
Keep reading

Related deep dives