Skip to content

CkWeb

Job: accept web addresses the safe way — cleaned, checked, normalized, and, before your app ever calls one, judged public or internal.

URLs that come from a form, a payload or a database row are just text until proven otherwise. CkWeb proves it: a value must parse as a real, absolute web address, use http or https (never javascript: or file:), and carry no username or password. What comes back is normalized — lower-case scheme and host, default ports dropped — so two spellings of the same address become the same string. And because a stored URL is the classic way an attacker points your server at your own internal network, IsPublicHost is here as the gate to call first. A value that cannot be used answers "" from the string methods and false from the yes/no ones — nothing throws unless you ask (see How it works).

Quick look

using SafeCoder.CommonKit;

CkWeb.Url("HTTPS://EXAMPLE.COM/Path?Q=1#f");      // -> "https://example.com/Path?Q=1#f"
CkWeb.Domain("https://api.example.com/v1/hook");  // -> "api.example.com"
CkWeb.IsHttps("http://example.com/");             // -> false
CkWeb.IsPublicHost("http://10.0.0.5/api");        // -> false  (internal address — do not call it)
CkWeb.Url("javascript:alert(1)");                 // -> ""  (not a web address — rejected)
CkWeb.InDomains(returnTo, "shop.example.com");    // -> true only for our own host — the redirect check
CkWeb.SameOrigin("https://Example.com:443/a", "https://example.com/b");   // -> true
CkWeb.AddQuery("https://example.com/?q=a", "page", "2");   // -> "https://example.com/?q=a&page=2"
CkWeb.Explain("example.com");                             // -> "not an absolute URL"

Methods

Url

Clean and validate an address.

string Url(
    string? input = null,
    bool requireHttps = false,
    string? fallback = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw URL.
requireHttps bool false Pass true to also refuse plain http, for endpoints that must be encrypted.
fallback string? null Backup URL used when the value is rejected. It goes through the same cleaning and validation, with the same options, and is never returned as typed; a fallback that is itself rejected answers "".
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkWebException that says why, instead of answering "", when neither the value nor the fallback can be used.

Returns

When Default With throwIfInvalid: true
The value (or the fallback) is a valid web address The normalized URL: "https://example.com/Path?Q=1#f" The normalized URL: "https://example.com/Path?Q=1#f"
Rejected: missing, over 2048 characters (CkSettings.MaxUrlInputLength), not absolute, not http(s), plain http with requireHttps: true, or credentials in the URL — https://user:pass@host/ is refused outright, and so is the empty marker of https://@host/ Empty string Throws CkWebException — carries Input, Expected and Reason

Validation proves the shape, not the destination — the host may not exist or may refuse the call.

CkWeb.Url("HTTPS://EXAMPLE.COM/Path?Q=1#f");   // -> "https://example.com/Path?Q=1#f"
CkWeb.Url("https://example.com:443/x");        // -> "https://example.com/x"  (443 is the https default port)
CkWeb.Url("https://example.com");              // -> "https://example.com/"   (an empty path becomes "/")
CkWeb.Url("example.com");                      // -> ""  (no scheme — not an absolute URL)
CkWeb.Url("https://user:pass@example.com/");   // -> ""  (credentials in the URL — refused)
CkWeb.Url(
    input: "http://example.com/hook",
    requireHttps: true
);                                             // -> ""  (plain http, and you asked for https only)

Note that only the scheme and host are lower-cased — the path and query keep their casing, because on many servers /Path and /path are different pages.

Domain

Just the host.

string Domain(
    string? input = null,
    string? fallback = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw URL.
fallback string? null Backup URL used when the value is rejected. It goes through the same cleaning and validation and is never returned as typed — the fallback is a URL, and its host is returned; a fallback that is itself rejected answers "".
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkWebException that says why, instead of answering "", when neither the value nor the fallback can be used.

Returns

When Default With throwIfInvalid: true
The value (or the fallback) is a valid web address The host, lower-case ASCII: "api.example.com" The host, lower-case ASCII: "api.example.com"
Rejected: missing, oversized, not absolute, not http(s), or credentials in the URL — checked the exact same way Url checks it Empty string Throws CkWebException — carries Input, Expected and Reason

An internationalized host (one with accented or non-Latin letters) comes back in its punycode form — the all-ASCII encoding that starts with xn-- — the form to store, compare and match against allow-lists, because two spellings of one host always agree in it.

CkWeb.Domain("https://api.example.com/v1/hook");  // -> "api.example.com"
CkWeb.Domain("HTTPS://BÜCHER.example.com/x");     // -> "xn--bcher-kva.example.com"  (the ASCII form)
CkWeb.Domain("not a url");                        // -> ""

IsHttps

Is the connection encrypted?

bool IsHttps(
    string? input = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw URL.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkWebException that says why, instead of answering false, when the value is not a usable URL.

Returns

When Default With throwIfInvalid: true
A valid https URL true true
A valid http URL false false
Rejected: not a usable URL false Throws CkWebException — carries Input, Expected and Reason

false does double duty — the XML docs say it comes back "for a valid http URL and for an unusable value alike"; with throwIfInvalid: true a broken value throws, so false can only mean "valid but http".

CkWeb.IsHttps("https://example.com/");  // -> true
CkWeb.IsHttps("http://example.com/");   // -> false
CkWeb.IsHttps("banana");                // -> false  (unusable — also false)

IsPublicHost

The SSRF gate.

bool IsPublicHost(
    string? input = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw URL.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkWebException that says why, instead of answering false, when the value is not a usable URL.

Returns

When Default With throwIfInvalid: true
A valid URL whose host looks public true true
A valid URL whose host points inside — loopback and its hosts-file aliases, private ranges, link-local, carrier-grade NAT, unique-local IPv6, multicast, unspecified, the documentation, benchmarking and IETF protocol-assignment blocks, an internal IPv4 wrapped in an IPv6 form, a single-label name, or a reserved suffix false false
Rejected: not a usable URL false Throws CkWebException — carries Input, Expected and Reason

Junk gets the safe answer — the XML docs say false comes back "for an internal host and for an unusable value alike", so a broken URL can never slip through as public.

Here is the attack this method stops. Say your app POSTs notifications to URLs that partners saved in your database. An attacker who can edit one of those rows changes it to http://10.0.0.5/admin — and now your server, sitting inside your network with all its access, makes that call for them. That trick is called SSRF (server-side request forgery). IsPublicHost is the cheap first gate: it answers false for every address that points inside — localhost and 127.0.0.1, the private ranges (10.x, 172.16–31.x, 192.168.x), link-local addresses like the cloud metadata service, single-label intranet names, and reserved name suffixes such as .local and .internal. Call it before POSTing to any stored URL.

CkWeb.IsPublicHost("https://api.example.com/webhooks");  // -> true
CkWeb.IsPublicHost("http://169.254.169.254/");           // -> false  (the cloud metadata address)
CkWeb.IsPublicHost("http://10.0.0.5/api");               // -> false  (private range)
CkWeb.IsPublicHost("http://localhost:8080/");            // -> false  (loopback)
CkWeb.IsPublicHost("http://intranet/");                  // -> false  (single-label name — internal)
CkWeb.IsPublicHost("https://printer.local/");            // -> false  (reserved suffix)

When the host is a bare IP address, the judgement comes from the same table CkIP uses — so the URL gate and the bare-address gate can never disagree.

IsUrl

Is it a usable web address?

bool IsUrl(
    string? input = null,
    bool requireHttps = false,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw URL.
requireHttps bool false Also refuse plain http, for endpoints that must be encrypted.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkWebException that says why, instead of answering false, when the value is not a usable URL.

Returns

When Default With throwIfInvalid: true
Absolute, http or https, no credentials — and https when required true true
Rejected: relative, another scheme, credentials, unusable false Throws CkWebException — carries Input, Expected and Reason

The yes-or-no form of Url. When the value is going to be stored, call Url instead and check for "" — same question, and you get the normalized address back.

CkWeb.IsUrl("HTTPS://EXAMPLE.COM");                 // -> true
CkWeb.IsUrl("example.com");                         // -> false  (not absolute)
CkWeb.IsUrl("http://example.com/", requireHttps: true);   // -> false

InDomains

Is the host on the allow-list?

bool InDomains(
    string? input = null,
    params IEnumerable<string?> domains
)
Parameter Type Default What it does
input string? null The raw URL.
domains params IEnumerable<string?> — The hosts to accept: names, *.name patterns, or full URLs whose host is taken. Null, empty and unreadable entries are skipped.

Returns

When You get
The value is a usable URL and its host matches an entry true
It is not a URL, no entry matches, or the list is empty false — an answer, not a failure; it never throws

The open-redirect defence and the webhook allow-list: run it on a "return to" parameter before redirecting, and on a stored endpoint before calling it, alongside IsPublicHost. Hosts are compared in ASCII form, lower-case, as Domain hands them out, so IDN spellings, casing and a trailing dot agree. *.example.com matches every sub-domain but not the apex, which needs its own entry — the same rule as CkEmail.InDomains. Suffix tricks fail: example.com.evil.com matches neither example.com nor *.example.com. A params list must be last, so there is no throwIfInvalid; call Url with it first when you need the exception.

CkWeb.InDomains("https://api.example.com/hooks", "*.example.com");    // -> true
CkWeb.InDomains("https://example.com/", "*.example.com");             // -> false  (apex needs its own entry)
CkWeb.InDomains("https://example.com.evil.com/", "example.com");      // -> false
CkWeb.InDomains(returnTo, "shop.example.com", "https://account.example.com/");   // the redirect check

SameOrigin

The same site, as a browser sees it?

bool SameOrigin(
    string? a = null,
    string? b = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
a string? null The first URL.
b string? null The second URL.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkWebException that says which value is not a usable URL and why, instead of answering false.

Returns

When Default With throwIfInvalid: true
Both are usable URLs with the same scheme, host and port true true
Both are usable, and any of the three differs false false
Rejected: either value is not a usable URL false Throws CkWebException — carries Input, Expected and Reason

Scheme, host and port, with default ports filled in and hosts compared in ASCII form: https://Example.com:443/a and https://example.com/b are the same origin; http://example.com/ and https://example.com/ are not, nor are example.com and www.example.com. The check behind "the redirect target must stay on our site" and "the callback matches the registered one". Two values that are not URLs are never the same origin.

CkWeb.SameOrigin("https://Example.com:443/a", "https://example.com/b");   // -> true
CkWeb.SameOrigin("https://example.com/", "http://example.com/");          // -> false  (scheme)
CkWeb.SameOrigin("https://example.com/", "https://www.example.com/");     // -> false  (host)

SameUrl

The same address, once normalized?

bool SameUrl(
    string? a = null,
    string? b = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
a string? null The first URL.
b string? null The second URL.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkWebException that says which value is not a usable URL and why, instead of answering false.

Returns

When Default With throwIfInvalid: true
Both are usable URLs with the same spelling after Url's normalization true true
Both are usable, and the spellings differ false false
Rejected: either value is not a usable URL false Throws CkWebException — carries Input, Expected and Reason

What Url(a) == Url(b) says, with both non-empty, as one call: lower-case scheme and host, default port dropped, an empty path written as /. Nothing cleverer — the path keeps its case, the query its order, the fragment counts, and a trailing dot on the host stays because Url keeps it. One exception, made because it is one name: an IDN host and its punycode spelling, bücher.de and xn--bcher-kva.de, are the same URL. For "the same site" use SameOrigin.

CkWeb.SameUrl("HTTPS://EXAMPLE.COM:443/Path?Q=1", "https://example.com/Path?Q=1");   // -> true
CkWeb.SameUrl("https://example.com/a", "https://example.com/A");                     // -> false  (path case)
CkWeb.SameUrl("https://example.com/x#a", "https://example.com/x#b");                 // -> false  (fragment)

IsIpHost

Is the host a literal IP address?

bool IsIpHost(
    string? input = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw URL.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkWebException that says why, instead of answering false, when the value is not a usable URL.

Returns

When Default With throwIfInvalid: true
A valid URL whose host is an IPv4 or IPv6 literal true true
A valid URL with a named host false false
Rejected: not a usable URL false Throws CkWebException — carries Input, Expected and Reason

http://127.0.0.1/ and https://[2001:db8::1]/ are literals; http://localhost/ is a name, even though it means loopback. IsPublicHost lets a public IP literal through, and many endpoint policies refuse literals outright: they sidestep DNS-based controls and never match a certificate. The host is classified by the framework's Uri parser, so exactly the spellings Url accepts are judged here.

CkWeb.IsIpHost("https://8.8.8.8/x");     // -> true   (public, but a literal)
CkWeb.IsIpHost("http://localhost/");     // -> false  (a name)

HasPort

Is a non-default port named?

bool HasPort(
    string? input = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw URL.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkWebException that says why, instead of answering false, when the value is not a usable URL.

Returns

When Default With throwIfInvalid: true
A valid URL naming a port other than its scheme's default true true
A valid URL on the default port, written or not false false
Rejected: not a usable URL false Throws CkWebException — carries Input, Expected and Reason

For policies that hold endpoints to the standard ports. https://example.com:443/ does not count — 443 is https' default and Url drops it — while http://example.com:443/ does.

CkWeb.HasPort("https://example.com:8443/");   // -> true
CkWeb.HasPort("https://example.com:443/");    // -> false  (the default)

HasQuery

Is there a query string?

bool HasQuery(
    string? input = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw URL.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkWebException that says why, instead of answering false, when the value is not a usable URL.

Returns

When Default With throwIfInvalid: true
A valid URL with something after its ? true true
A valid URL with no query, or a bare ? false false
Rejected: not a usable URL false Throws CkWebException — carries Input, Expected and Reason

For endpoints that must not carry parameters, and for spotting tracking tails on stored links. A fragment is not a query.

CkWeb.HasQuery("https://example.com/?utm_source=x");   // -> true
CkWeb.HasQuery("https://example.com/#top");            // -> false

AddQuery

Set a query parameter, encoded.

string AddQuery(
    string? input = null,
    string? key = null,
    string? value = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw URL.
key string? null The parameter name.
value string? null The parameter value; null writes the key with an empty value.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkWebException that says why, instead of answering "", when the URL is not usable, the key is missing or the result would be too long.

Returns

When Default With throwIfInvalid: true
A usable URL and a key The URL with the parameter set — replaced if it was there — the fragment kept The URL
Rejected: an unusable URL, no key, or a result over 2048 characters (CkSettings.MaxUrlInputLength) "" Throws CkWebException — carries Input, Expected and Reason

The key and the value are encoded so that spaces, ampersands and Unicode travel safely, which string concatenation gets wrong. Calling twice does not double the parameter. A + in an existing key reads as a space, as CkPayload.FromQuery reads it. What AddQuery writes, Url reads back: the result is held to the same 2048 characters, and a parameter that would take it over is refused — "over 2048 characters once the parameter is added" — rather than handed out as a URL the library itself would then reject.

CkWeb.AddQuery("https://example.com/search?q=a", "page", "2");   // -> "https://example.com/search?q=a&page=2"
CkWeb.AddQuery("https://example.com/", "next", "/a?b=1");         // -> "https://example.com/?next=%2Fa%3Fb%3D1"

RemoveQuery

Drop a query parameter.

string RemoveQuery(
    string? input = null,
    string? key = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw URL.
key string? null The parameter name to remove.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkWebException that says why, instead of answering "", when the URL is not usable or the key is missing.

Returns

When Default With throwIfInvalid: true
A usable URL and a key The URL without that parameter, every occurrence, the ? going too when nothing is left The URL
Rejected: an unusable URL, no key "" Throws CkWebException — carries Input, Expected and Reason

A key that is not there changes nothing, which is an answer, not an error. For stripping tracking tails before a link is stored.

CkWeb.RemoveQuery("https://example.com/?q=a&utm_source=x", "utm_source");   // -> "https://example.com/?q=a"

Explain

Why not — in words, without an exception.

string Explain(
    string? input = null,
    bool requireHttps = false
)
Parameter Type Default What it does
input string? null The raw URL.
requireHttps bool false Also refuse plain http.

Returns

When You get
The value passes the reader ""
It does not The reason, the same words the exception would carry — an answer, not a failure; it never throws

The form-friendly twin of throwIfInvalid: the message to show beside the field, without a try and catch. Every class with a reader has one, on the reader's own rules, so the reason is exactly what the reader would say.

CkWeb.Explain("example.com");                    // -> "not an absolute URL"
CkWeb.Explain("https://user:pass@example.com/");  // -> "credentials in the URL"

The fallback

The two string methods, Url and Domain, take a fallback — a spare URL used when the input is rejected. The fallback goes through the exact same cleaning and checking (with the same options); a bad fallback answers "" too. The yes/no methods have no fallback — they simply answer false.

CkWeb.Url(
    input: "not a url",
    fallback: "https://example.com/default"
);   // -> "https://example.com/default"

Good to know

  • IsPublicHost judges the spelling, not the destination. A hostile DNS name that resolves to an internal address passes this gate. It is the cheap check at intake, not the whole defence — when you actually make the request, resolve the name and check the address you are really dialling.
  • The loopback aliases count as loopback. IsPublicHost refuses localhost and with it the names a hosts file ships beside it — localhost.localdomain, localhost4, localhost6.localdomain6 — and the suffixes .localhost, .localdomain, .local, .internal and .home.arpa; a real name that merely begins with the word (localhostx.com) is a name. An internal IPv4 wrapped as IPv6 — mapped, NAT64, 6to4, Teredo — is judged as the address inside.
  • Url proves the shape, not that anyone answers. A perfectly valid URL may point at a host that does not exist or refuses the call.
  • Only http and https count. javascript:, file:, ftp: and friends are not web endpoints; they answer "".
  • Credentials are always refused. There is no switch to allow https://user:pass@host/ — store secrets elsewhere.
  • false is doing double duty. IsHttps, IsPublicHost, IsIpHost, HasPort, HasQuery, SameOrigin and SameUrl answer false both for "no" and for "not a usable URL". Validate with Url first, or pass throwIfInvalid: true, when the difference matters. InDomains never takes the flag.
  • InDomains is the open-redirect check. Run it on any "return to" or "next" parameter before redirecting, with your own hosts as the list; suffix tricks like example.com.evil.com fail it. SameOrigin is the stricter form when the scheme and port must match too.
  • SameUrl is exact after normalization. Case in the path, the order of query parameters and the fragment all count; SameOrigin is the comparison that ignores everything past the port.
  • Non-empty in, empty out means rejected. That is the standard CommonKit signal for the string methods — check for "" where it matters.

If you want errors instead

Pass throwIfInvalid: true and a value that cannot be used raises a CkWebException (from SafeCoder.CommonKit.Exceptions) instead of answering "" or false. The exception carries the Input and a short Reason — "not an absolute URL", "credentials in the URL" — flattened into a one-line message.

CkWeb.Url(
    input: "ftp://example.com/file",
    throwIfInvalid: true
);   // throws CkWebException — the message says why