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
IsPublicHostjudges 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.
IsPublicHostrefuseslocalhostand with it the names a hosts file ships beside it —localhost.localdomain,localhost4,localhost6.localdomain6— and the suffixes.localhost,.localdomain,.local,.internaland.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. Urlproves 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. falseis doing double duty.IsHttps,IsPublicHost,IsIpHost,HasPort,HasQuery,SameOriginandSameUrlanswerfalseboth for "no" and for "not a usable URL". Validate withUrlfirst, or passthrowIfInvalid: true, when the difference matters.InDomainsnever takes the flag.InDomainsis the open-redirect check. Run it on any "return to" or "next" parameter before redirecting, with your own hosts as the list; suffix tricks likeexample.com.evil.comfail it.SameOriginis the stricter form when the scheme and port must match too.SameUrlis exact after normalization. Case in the path, the order of query parameters and the fragment all count;SameOriginis 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