Lichess · Report #3165242
Server-Side Request Forgery (SSRF) via Game Export API
How it was found
Summary
Hello Lichess Team,
I found a Server-Side Request Forgery vulnerability in the game export functionality. An attacker can make the Lichess server send HTTP requests to arbitrary URLs by manipulating the players parameter. This works on public endpoints that don't require any authentication
Description:
The issue is in the game export API endpoints that accept a players parameter. When I looked at the code, I noticed that this parameter gets passed directly to RealPlayerApi.apply() without any URL validation
Here's the problematic flow I found:
In app/controllers/Game.scala:
val config = GameApiV2.OneConfig(
format = GameApiV2.Format.byRequest,
imported = getBool("imported"),
flags = requestPgnFlags(extended = true),
playerFile = get("players") // User input taken directly
)
Then in modules/api/src/main/GameApiV2.scala:
realPlayers <- config.playerFile.so(realPlayerApi.apply)
And finally in modules/web/src/main/RealPlayer.scala
def apply(url: String): Fu[Option[RealPlayers]] = cache.get(url)
// This leads to:
ws.url(url).withRequestTimeout(3.seconds).get()
The get("players") method directly reads from HTTP query parameters without any validation. The so() method executes the function when the Option contains a value, so any URL I provide gets passed straight to the HTTP client
PoC
Note: I discovered this vulnerability through source code analysis and then confirmed it by testing live on the official Lichess website. The following PoC demonstrates the issue as it exists in the production environment
- Pick any valid game ID from Lichess (like from a recent game)
{F4390928}
-
Make a request to:
/game/export/[GAME_ID]?players=http://169.254.169.254/latest/meta-data/ -
The server will make an HTTP request to the provided URL
-
You can also test with:
/api/games/export/_ids?players=[URL]or/api/games/user/[USERNAME]?players=[URL] -
Monitor your server logs or use a service like webhook.site to confirm the request
{F4390944}
{F4390945}
Mitigation:
To fix this, you should:
-
Add URL validation to only allow specific trusted domains for the
playersparameter -
Implement a whitelist of allowed URLs or URL patterns
-
Consider removing the feature entirely if it's not critical
-
Add authentication requirements for these endpoints
-
Block requests to private IP ranges (127.x.x.x, 192.168.x.x, 10.x.x.x, 169.254.x.x) (For security and ethical reasons, I did not send any actual requests to these IP addresses. If required, I can provide my own IP address or any additional information for verification purposes)
The fix should be in RealPlayerApi.apply() method to validate the URL before making any HTTP requests.
https://cwe.mitre.org/data/definitions/918.html
Impact
An attacker could use this to:
- Access cloud metadata services (AWS, GCP) to steal credentials and configuration
- Scan internal networks and discover internal services
- Access internal APIs and admin panels that aren't exposed to the internet
- Potentially read sensitive internal data or configuration files
- Perform port scanning of internal infrastructure
This is particularly concerning because the endpoints are public - no authentication needed. Also, since it's on a high-traffic site like Lichess, an attacker could potentially use this to bypass IP-based restrictions on internal services