A small, self-hosted favicon resolver. Given a domain, Sigil fetches that
site's real favicon server-side, normalizes it to a PNG at the size you
ask for, and caches the result. It exists to replace third-party favicon
proxies (e.g. google.com/s2/favicons) so that resolving an icon never leaks
which domains your users look up to an outside party.
Apache-2.0. Sigil fetches URLs derived from untrusted domains — an SSRF surface by design — so a hardened fetch boundary is its headline feature (see the Security model and
THREAT-MODEL.md). Self-reviewed, not independently audited.
Sigil's whole reason to be a separate service is containment: the code that fetches attacker-influenceable URLs must not run in a process that holds database credentials or an internal-network position. The running instance is designed to have zero internal secrets and egress-only reach — if its defenses were ever bypassed, the blast radius is a box that can talk to the public internet and nothing else.
The fetch boundary (pkg/favicon/fetch.go) enforces, for every request and
every redirect hop:
- Dial-time IP validation (TOCTOU-safe). The resolved IP is checked in
net.Dialer.Control— after DNS resolution, immediately before connect, once per candidate address — usingcode.dny.dev/ssrf(an IANA-registry-derived denylist). This closes the resolve-then-connect gap and defeats DNS rebinding: a name that rebinds between check and dial is still validated at the dial. Validation is onnetip.Addr, so IPv4-mapped IPv6 (::ffff:169.254.169.254) cannot slip past as global unicast. - A default-deny address policy. Loopback, RFC 1918 private space, CGNAT
(
100.64.0.0/10), link-local169.254.0.0/16(the cloud metadata endpoint), multicast, reserved ranges, IPv6 ULA/link-local, and NAT64 are all rejected. Only TCP to ports 80/443 is permitted. - Bounded redirects. At most 3 hops, and every hop must be
http/https— afile://,gopher://, etc. redirect is refused. - Bounded responses. The body is read through a hard byte cap, the content type is decided by sniffing (never the response header), only raster image types are accepted, and the decoded pixel dimensions are bounded before full decode to defeat decompression bombs.
- SVG only through a sandboxed rasterizer. An SVG is XML that can carry
script (stored-XSS risk) or external entities (XXE / second-order SSRF), so
SVG bytes are never served or stored: a structurally-detected SVG is bounded
(bytes, XML tokens, nesting depth) and rendered to a fixed-size pixel buffer
by a pure-Go renderer (
oksvg+rasterx) built onencoding/xml, which has no code path that loads external entities, DTDs, or URLs — the XXE class cannot be expressed, and a no-network regression test keeps it that way. Renderer panics are contained per candidate. The output, like every icon Sigil serves, is a freshly encoded PNG.
The SSRF test suite (pkg/favicon/fetch_test.go) asserts each of these
independently of the guard library, so a dependency bump that regressed a range
would fail our tests rather than silently ship.
- Is: a hardened favicon fetcher + normalizer, usable as a Go library
(
github.com/ciphera-net/sigil/pkg/favicon) or as a tiny HTTP service (cmd/server). - Is not: a public favicon API for the internet, a general URL fetcher, or a proxy. The published instance stays internal; publishing the code is the open-source act.
go get github.com/ciphera-net/sigil/pkg/favicon
Requires Go 1.25+.
| Route | Response |
|---|---|
GET /icon?domain=<hostname>&sz=<16|32|64|128> |
200 image/png resolved icon · 404 no icon (negatively cached) · 400 bad domain/size · 502 upstream fetch failure (not cached) |
GET /healthz |
liveness |
GET /metrics |
Prometheus |
domain must be a bare hostname (no scheme, port, path, or IP literal).
docker build -t sigil .
docker run -p 8085:8085 sigil
The container is a distroless static image holding only the server binary.
.woodpecker/ runs, as separate checks: test (gofmt, go vet,
go test -race, including the SSRF suite), govulncheck (dependency-vuln
scan), and — once the service lands — build / push / deploy.
Licensed under the Apache License, Version 2.0 — see LICENSE. Contributions require a DCO sign-off; see CONTRIBUTING.md.