| Safe Haskell | None |
|---|---|
| Language | GHC2021 |
Ecluse.Core.Security.Url
Contents
Description
Outbound-request guards for the proxy's data plane: defending how an upstream URL is derived.
Écluse builds outbound HTTP requests from two untrusted sources -- __client-supplied
package identifiers (the request path) and upstream-supplied artifact
locations__ (a packument's dist.tarball). This module provides the pure guard layer
that enforces safe URL construction.
How an upstream URL is derived: upstreamUrlFor builds an artifact/metadata
URL from a configured base URL and an already-parsed PackageName, never
from raw client path segments, re-checking each name component with the router's
own safety rule so traversal, encoded slashes, or an absolute URL cannot change
the target.
Synopsis
- upstreamUrlFor :: Text -> PackageName -> Either UrlError Text
- data UrlError
Identifier → URL safety
upstreamUrlFor :: Text -> PackageName -> Either UrlError Text Source #
Build an upstream URL for a package from a configured base URL and an
already-parsed PackageName.
This is the only sanctioned way to derive an upstream URL for a package: the
target is {baseUrl}/{path}, where path is built from the package's structural
components and baseUrl is configuration, never a client-supplied path. The
client never chooses the host or the path prefix -- only which (validated) package
-- so ../ traversal, an encoded slash, an absolute URL, or a CRLF in the original
request cannot steer the fetch elsewhere (see the module header).
The path is built with two complementary defences. First, although a PackageName
is normally produced by the router's already-safe parse, its smart constructor does
no validation, so this re-checks every structural component (scope and base
name) with the router's own isSafeComponent -- a name carrying
a '/', '\\', control character, or a "."/".." component is refused
with UnsafeComponent rather than interpolated. Second, each accepted component is
then percent-encoded (encodeComponent) around the
structural '@' sigil and %2F scope separator this builder writes -- so a
'%', '?', '#', or other reserved byte the denylist accepts (notably a
once-decoded %2e%2e%2f) cannot reach the upstream URL raw. A scoped
@scope/name therefore yields exactly one %2F (the separator written here, not
an encoding of a component), with no double-encoding. An empty baseUrl is refused
with EmptyBaseUrl. A single trailing slash on baseUrl is tolerated so the join
never doubles it.
Why building an upstream URL from an identifier was refused.
Constructors
| UnsafeComponent Text | A name component (scope or base name) is unsafe to interpolate -- see
|
| EmptyBaseUrl | The configured base URL is empty, so no URL can be formed. |