ecluse:ecluse-core
Safe HaskellNone
LanguageGHC2021

Ecluse.Core.Security.Authority

Description

Textual extraction of the host[:port] authority an outbound request dials.

These are comparison extractors, not an RFC 3986 parser: a value with no recognisable authority yields the empty string or Nothing, which every guard treats as not-allowed. The SSRF gates in Ecluse.Core.Security.Host consume the extracted HostPort, so the parsing here carries no policy of its own.

Synopsis

The dialled authority

data HostPort Source #

The authority an outbound fetch dials: a bare host with its effective port. The gate authorises the pair, so an allowlisted host at an attacker-chosen port is not authorised.

Constructors

HostPort 

Fields

  • hpHost :: Text

    The bare host: no brackets, no port, lower-cased by both extractors.

  • hpPort :: Word16

    The effective port: the explicit :port, or the caller's portless default.

Instances

Instances details
Show HostPort Source # 
Instance details

Defined in Ecluse.Core.Security.Authority

Eq HostPort Source # 
Instance details

Defined in Ecluse.Core.Security.Authority

Ord HostPort Source # 
Instance details

Defined in Ecluse.Core.Security.Authority

Authority extraction

hostAddress :: Text -> Text Source #

The bare host of a URI or host[:port] authority, lower-cased and unbracketed. A value with no recognisable host yields the empty string, which the guards treat as not-allowed.

hostPortAddress :: Text -> Maybe HostPort Source #

The host and the effective port a URI or host[:port] authority dials, or Nothing when it holds none. A portless URL dials 443, and an out-of-grammar or written-but-empty port is refused.

>>> hostPortAddress "https://[2606:4700::1111]:8443/thing"
Just (HostPort {hpHost = "2606:4700::1111", hpPort = 8443})

hostPortAddressWithDefault :: Word16 -> Text -> Maybe HostPort Source #

hostPortAddress with the caller's own port for a URL that writes none, for a scheme whose portless default is not the gate's 443. The authority split and the port grammar do not change.

splitHostPort :: Text -> Maybe (Text, Text) Source #

Split a host[:port] authority into its bare host and the raw ":port" remainder, empty when no port is present. The split is bracket-aware, and an unclosed opening bracket yields Nothing.

Configured-URL refusal

refuseCredentialMaterial :: Text -> Text -> Either Text () Source #

Refuse an operator-configured URL that carries credential material: userinfo, a query string, or a fragment. Run it before any check that quotes the value, and the reason it returns never does.

>>> refuseCredentialMaterial "server.publicUrl" "https://deploy:hunter2@ecluse.example.test"
Left "server.publicUrl must not carry userinfo (a credential belongs in its own configuration key)"
>>> refuseCredentialMaterial "registry.url" "https://registry.npmjs.org/@acme/thing"
Right ()

Log-safe rendering

authorityLabel :: Text -> Text Source #

The log-safe label for a URL: its validated host and effective port, or <unresolved>. An attacker-influenced or credential-bearing URL must never reach a log line or a span as written.

>>> authorityLabel "https://deploy:hunter2@registry.npmjs.org/thing/-/thing-1.0.0.tgz?sig=abc"
"registry.npmjs.org:443"

dialledAuthorityLabel :: Text -> Text Source #

authorityLabel for a URL a client dials as written, so a portless http:// URL names port 80.

>>> dialledAuthorityLabel "HTTP://mirror.example.test/epss.csv.gz"
"mirror.example.test:80"

credentialFreeUrl :: Text -> Text Source #

A fetched URL with every credential carrier removed: userinfo, query, and fragment, the last two whole, because either can hold a signed-URL credential.

>>> credentialFreeUrl "https://deploy:hunter2@osv.example.test/npm/all.zip?sig=abc#frag"
"https://osv.example.test/npm/all.zip"