Image Hosts
Last updated
May be stale
This page was last updated ago, on 4 October 2026. Check what it says against the code before relying on it.
Players can point several systems at an image URL and have the server or every other client fetch it. Each of those systems used to carry its own copy of the "safe hosts" list, and the copies drifted — the billboard UI advertised a host the validator rejected, and the 3D sign editor accepted a host the Material URL tool had already dropped.
There is now one list, in
gamemode/core/libraries/sh_imagehosts.lua, exposed as cityrp.imageHosts. It is shared
(client and server), loads with the core libraries, and is the only place a host name should
appear.
Maintaining the list
Edit cityrp.imageHosts.definitions in
sh_imagehosts.lua,
which is the current list; it is not repeated here so this page cannot drift from it. Each entry
is one host:
{host = "i.ibb.co", enabled = true, reason = "ImgBB direct image host."},
- Set
enabled = falserather than deleting an entry, so the reason a host was dropped stays on record. Disabled entries are invisible to every consumer. - Give the exact hostname players will link to. Matching is exact and case-insensitive;
subdomains are not implied, so
i.ibb.codoes not permitevil.i.ibb.co. - Prefer hosts that serve the raw image bytes.
imgur.com/abcreturns an HTML page;i.imgur.com/abc.pngreturns the image. - Mark a host
extensionless = trueonly when it serves images from paths with no file extension, as Steam user content does (images.steamusercontent.com/ugc/<id>/<hash>/). Systems that require an image extension then accept that host's extensionless URLs, and rely on checking the downloaded file instead. An extension that is present still has to be an accepted one.
Accepted file extensions live alongside it in cityrp.imageHosts.extensions.
The UIs that list approved hosts read them from here, so no UI text needs updating when the
list changes. Player-facing prose goes through getHostList() / getExtensionList(), which
use FormatList so the wording matches the rest of the gamemode; UI that lays the hostnames
out itself (one per line, bullet-separated) uses getHosts() and does its own formatting.
Using the list
Validate whole URLs with cityrp.imageHosts.validate, which normalises the input and returns
a player-facing reason on rejection:
local url, reason = cityrp.imageHosts.validate(input, true) -- true = also require png/jpg/jpeg
if not url then
ply:Notify(reason, NOTIFY_ERROR)
return
end
Other entry points:
| Function | Purpose |
|---|---|
cityrp.imageHosts.validate(url, requireExtension) |
Normalise and check a URL. Returns the normalised URL, or nil plus a reason. requireExtension applies hasImagePath. |
cityrp.imageHosts.isAllowed(url) |
Boolean check of a full URL. |
cityrp.imageHosts.isAllowedHost(host) |
Boolean check of a bare hostname. |
cityrp.imageHosts.normalise(url) |
Lowercase the scheme, assume https:// when none is given, keep any trailing slash (Steam user-content URLs 404 without it), reject anything that is not http or https. Does not check the allowlist. |
cityrp.imageHosts.getHost(url) |
Parse the hostname out of a URL. |
cityrp.imageHosts.hasAllowedExtension(url) |
Check the file extension only. |
cityrp.imageHosts.hasImagePath(url) |
An accepted extension, or none at all on an extensionless host. A consumer accepting the second case must check the downloaded bytes or content type. |
cityrp.imageHosts.getHosts() |
Copy of the enabled hostnames, for UI that lays them out itself. |
cityrp.imageHosts.getHostList(conjunction) |
The enabled hostnames as prose, via FormatList — "a, b or c". |
cityrp.imageHosts.getExtensionList(conjunction) |
The accepted extensions as prose, the same way. |
Schemes
Both http and https are accepted, and a URL with no scheme is assumed to be https.
Plain http means the image bytes can be tampered with in transit by anything sitting between
the server and the host. That is a deliberate, accepted trade-off rather than an oversight: the
host is still allowlisted, the response is only ever decoded as an image, and enough approved
links arrive as http that rejecting them broke more than it protected. Anything that is neither
http nor https — ftp://, javascript:, data:, a protocol-relative //host/path — is
rejected.
A URL carrying userinfo (user@host) or an explicit port is also rejected outright rather than
parsed. That is not new hardening — the per-system pattern matches this library replaced
already rejected both, because they required / or end-of-string straight after the hostname.
It is called out here because parsing the authority instead of pattern-matching it is exactly
where that property gets dropped by accident, and dropping it would let a crafted authority
read as one host to the validator and another to whatever fetches the URL.
Current consumers
| System | Where it validates | How the download is checked |
|---|---|---|
Billboards (plugins/billboards) |
sh/zad_url_validation.lua wraps the library; the config UI lists the hosts from it. |
cl/zad_imageloader.lua requires a png or jpeg Content-Type. |
Material URL tool (addons/material_url_2559426779) |
lua/autorun/materialurl_whitelist.lua bridges MatURL.checkURL and MatURL.checkFormat onto the library. |
The client sniffs the file's magic bytes and Content-Type before baking a material. |
3D signs (addons/3dsign, entities/cityrp_3d_sign) |
The editor validates as you type; the apply net messages re-check server-side. | Rendered by an HTML panel's <img>, which draws only what decodes as an image. |
The Material URL bridge runs from lua/autorun, which loads before the gamemode. It
therefore resolves cityrp.imageHosts at call time and denies the URL if the library is not
loaded yet, rather than caching a snapshot. Any addon-side consumer added later needs to do
the same.
What this does not cover
rRadio keeps its own allowlist of audio stream hosts (rRadio.config.AllowedHosts). Those are
stream endpoints with different rules — plain http is normal for them, and the list is
maintained against the station packs — so they are deliberately not merged into
cityrp.imageHosts.