Catching scraping vectors before they ship
Meta's Anti-Scraping team is responsible for stopping unauthorized data collection across Facebook, Instagram, and parts of Reality Labs. Scraping is the automated harvesting of data from an app or website, and unauthorized scrapers often disguise themselves by behaving like regular users. That makes detection difficult, so the team has moved some of its defenses earlier in the development cycle by adding static analysis rules to its in-house security tools, Zoncolan for Hack and Pysa for Python.
These tools run automatically across Meta's codebases, flagging potential scraping vectors before code reaches production. The approach turns known attack patterns into automated checks, giving product teams a chance to fix issues during development instead of after an incident.
How the rules work
Both Zoncolan and Pysa track data flow through a program. Engineers describe classes of problems using three concepts:
- Sources are where data originates. For scraping, these are typically user-controlled parameters, since they are the input points a scraper can manipulate.
- Sinks are where data flows. For scraping, the relevant sink is data being returned to the user.
- An issue is reported when the tools detect a possible path from a source to a sink.
Consider a user-controlled count parameter that decides how many results an endpoint returns. That parameter is an entry point for a scraper who can alter its value to request far more data than the application intends. When the static analysis suspects data can flow from such a parameter to a response, it alerts the team for triage.
A concrete example
The following mock code shows an endpoint that loads the number of followers for a page:
# views/followers.py
async def get_followers(request: HttpRequest) -> HttpResponse:
viewer = request.GET['viewer_id']
target = request.GET['target_id']
count = request.GET['count']
if(can_see(viewer, target)):
followers = load_followers(target, count)
return followers
# controller/followers.py
async def load_followers(target_id: int, count: int):
...
Here, both user and count are attacker-influenced: user selects whose follower list is returned, and count controls how many entries come back. A legitimate client sends values matching what the user is currently viewing. A scraper, however, could request arbitrary users with a very large count, pulling entire follower lists in a single request and potentially bypassing rate limits that are designed to cap request volume.
Because the static analysis runs continuously, the Anti-Scraping team can spot this pattern before the endpoint goes live. A typical remediation caps the number of results per request:
# views/followers.py
async def get_followers(request: HttpRequest) -> HttpResponse:
viewer = request.Get['viewer_id']
target = request.GET['target_id']
count = min(request.GET['count'], MAX_FOLLOWERS_RESULTS)
if(can_see(viewer, target)):
followers = load_followers(target, count)
return followers
# controller/followers.py
async def load_followers(target_id: int, count: int):
...
With the fix, each request is limited to MAX_FOLLOWERS_RESULTS results. Legitimate users are unaffected, while scrapers must send far more requests, which makes them easier to catch with rate limiting systems.
Limitations of static analysis
Static analysis is not a complete solution. Unauthorized scrapers can closely mimic legitimate product usage, and preventing all scraping would require breaking functionality that normal users rely on. Meta continues to combine proactive code analysis with reactive measures such as investigating suspected scraping activity, sending cease-and-desist letters, and disabling abusive accounts.



