Validates and resolves redirect URLs to prevent Open Redirect vulnerabilities.
valid_url?/2returns whether a URL is allowed.resolve_url/3returns the URL if it is allowed, or a default value if it is not.redirect/4resolves the URL and performs the redirect.
Allowed redirect URIs
All functions take an :allowed_redirect_uris option. The value is a list of
strings or URI structs, a single string or URI struct, or a
{module, function} tuple that returns such a list. The function is called
every time a URL is validated.
SafeRedirect.valid_url?(url,
allowed_redirect_uris: ["https://good.example"]
)
SafeRedirect.valid_url?(url,
allowed_redirect_uris: {MyAppWeb.RedirectURIs, :allowed_redirect_uris}
)If the option is omitted, the value is read from the application environment at call time, defaulting to an empty list.
config :safe_redirect,
allowed_redirect_uris: {MyAppWeb.RedirectURIs, :allowed_redirect_uris}The application environment is a global default for the common case of one
allowlist per system, and the option overrides it wherever a call needs
something else. It is read on every call rather than at compile time, so it
can be set in runtime.exs.
Validation rules
- Relative paths starting with
/are allowed without checking the allowed URIs. - Absolute URLs are allowed if the scheme, host, and port match one of the allowed URIs. The scheme and host are compared case-insensitively.
- A trailing dot is part of the host, so
https://good.example.does not match an allowedhttps://good.example. - Protocol-relative URLs starting with
//are not allowed. - A URL with a scheme but no host is not allowed.
mailto:,javascript:, anddata:URLs are always refused. - Paths must not contain dot segments (
.or..), literal or encoded. - Paths must not contain control characters, literal or encoded.
- Percent-encoded characters are decoded before a path is checked, so
/some%2Fpathis treated as/some/path. - An allowed URI may not have a path, query string, fragment, or userinfo, since only the scheme, host, and port are compared.
An invalid :allowed_redirect_uris option raises ArgumentError in every
function.
A URL given as a string is parsed with URI.new/1, which rejects a host that
is not ASCII. A URI struct is taken as given. If you build a URI struct
from a string, use URI.new/1 rather than URI.parse/1, which does not
reject such a host.
Summary
Types
The value of the :allowed_redirect_uris option.
Options accepted by all functions.
A URL, either as a string or as a URI struct.
Types
@type allowed_redirect_uris() :: [uri_source()] | uri_source() | {module(), atom()}
The value of the :allowed_redirect_uris option.
See the module documentation for the accepted shapes.
@type opts() :: [{:allowed_redirect_uris, allowed_redirect_uris()}]
Options accepted by all functions.
A URL, either as a string or as a URI struct.
Functions
@spec redirect(Plug.Conn.t(), term(), uri_source(), opts()) :: Plug.Conn.t()
@spec redirect( Phoenix.LiveView.Socket.t(), term(), uri_source(), opts() ) :: Phoenix.LiveView.Socket.t()
Resolves the given URL and performs an internal or external redirect.
Resolves url against the allowed URIs with resolve_url/3, falling back
to default, then redirects to the result.
Accepts a Plug.Conn or, when Phoenix.LiveView is available, a
Phoenix.LiveView.Socket, and returns the same type it was given.
A root-relative path is an internal redirect and an absolute http or
https URL an external one. For a LiveView socket these are passed to
Phoenix.LiveView.redirect/2 as to: and external:; for a Plug.Conn
both set the location header.
Given a Plug.Conn, the connection is halted.
The default value is not validated against the allowed URIs, so it must not come from user input.
Raises ArgumentError if the resolved URL is neither a relative path nor
an http or https URL, for example if the default value is nil or if
an allowed URI uses a different scheme. That is the only check applied to
the default value.
Examples
Using configuration via application environment:
redirect(conn, url, "/default")Passing allowed URIs directly:
redirect(
conn,
url,
"/default",
allowed_redirect_uris: ["https://good.example"]
)
@spec resolve_url(term(), uri_source() | nil, opts()) :: uri_source() | nil
Returns the given URL if it is a valid redirect URL or the default value otherwise.
The URL is returned unchanged, so passing a URI struct returns a URI
struct. Any other value, including nil, returns the default value.
The default value is returned as given and is not validated against the allowed URIs, so it must not come from user input.
See the module documentation for the validation rules and the
:allowed_redirect_uris option.
Examples
iex> url = "https://good.example/login"
iex> resolve_url(url, "/", allowed_redirect_uris: ["https://good.example"])
"https://good.example/login"
iex> url = "/login"
iex> resolve_url(url, "/", allowed_redirect_uris: ["https://good.example"])
"/login"
iex> url = "https://evil.example/login"
iex> resolve_url(url, "/", allowed_redirect_uris: ["https://good.example"])
"/"
@spec valid_url?(uri_source() | nil, opts()) :: boolean()
Takes a URL as a string or URI struct and determines whether it points to
an allowed host.
Relative paths are allowed without checking the allowed URIs. See the module
documentation for the validation rules and the :allowed_redirect_uris
option.
Examples
iex> url = "https://good.example/login"
iex> valid_url?(url, allowed_redirect_uris: ["https://good.example"])
true
iex> url = "/login"
iex> valid_url?(url, allowed_redirect_uris: ["https://good.example"])
true
iex> url = "https://evil.example/login"
iex> valid_url?(url, allowed_redirect_uris: ["https://good.example"])
false