Server-side request forgery (SSRF)
Need
Restriction of server-side outbound requests to trusted destinations
Context
• Usage of Elixir 1.15+ for building application services
• Usage of Plug.Router for handling HTTP requests
• Usage of the Finch HTTP client for outbound requests
Description
1. Non compliant code
defmodule MyAppWeb.PreviewRouter do
use Plug.Router
plug :match
plug :fetch_query_params
plug :dispatch
get "/preview" do...The `/preview` route below reads the `url` query parameter and fetches it with `Finch.build/2` and `Finch.request/2`, returning the response body to the caller. The server makes the request from its own network position, so the caller can point it at addresses that are not reachable from the internet: the cloud metadata service at `169.254.169.254`, which can return temporary credentials, internal admin panels, other services in the cluster, or ports on `localhost`. Because the body is returned, the attacker can also read what those internal services answer. The same weakness appears with `HTTPoison.get/1`, `Tesla.get/1` and `Req.get/1`, and when request data selects the host of a Redis or MongoDB connection string that carries the application's own credentials, which sends those credentials to a machine the attacker controls.
2. Steps
• Do not pass request parameters directly to `Finch.build`, `HTTPoison.get`, `Tesla.get` or `Req.get`; when a caller must choose a destination, accept an identifier and map it to a URL on the server.
• If a host must come from the request, parse it with `URI.new/1` and accept only the `https` scheme, the expected port, no user information and hosts from an explicit allowlist.
• Build the request from the parsed and validated URI rather than from the original string.
• Keep redirects disabled, or validate every redirect target against the same allowlist.
• Read Redis, MongoDB and database hosts from runtime configuration, never from request parameters.
• Block egress from the service to link-local, loopback and private address ranges at the network layer, including the cloud metadata endpoint.
3. Secure code example
defmodule MyAppWeb.PreviewRouter do
use Plug.Router
@allowed_hosts ["api.example.com", "status.example.com"]
plug :match
plug :fetch_query_params
plug :dispatch...The corrected router only fetches URLs that pass `allowed_uri/1`. The function parses the value with `URI.new/1` and accepts it only when the scheme is `https`, the port is 443, there is no user information such as `user@` before the host, and the host is one of the names in `@allowed_hosts`. Values such as `http://169.254.169.254/`, `https://localhost:8080` or `https://api.example.com@evil.example` are rejected with `400 Bad Request`. The request is then built from the parsed URI, so the value that was checked is exactly the value that is fetched. Finch does not follow redirects, which prevents an allowed host from bouncing the request to an internal address. For database and cache connections, the equivalent fix is to read the host from runtime configuration and never from the request.
References
• 100. Server-side request forgery (SSRF)