ecluse:ecluse-core
Safe HaskellNone
LanguageGHC2021

Ecluse.Core.Security.Url

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

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.

data UrlError Source #

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 isSafeComponent. Carries the offending component.

EmptyBaseUrl

The configured base URL is empty, so no URL can be formed.

Instances

Instances details
Show UrlError Source # 
Instance details

Defined in Ecluse.Core.Security.Url

Eq UrlError Source # 
Instance details

Defined in Ecluse.Core.Security.Url