Refuse image requests whose Referer is on referer_blocklist (closes #90)
A new setting, referer_blocklist (PIXA_REFERER_BLOCKLIST), lists hosts written and matched as allowlist_hosts are, with the same matcher and the same entry check; a bad entry aborts startup naming the setting and the entry. Both image routes check the Referer first and answer 403 with the JSON error, so a blocked request fetches nothing and is refused whether or not the image is cached. No Referer, or one that does not parse as a URL with a host, is served; README.md and config.example.yml say this makes the list easy to get around. The CIDR-list entry reader is renamed listEntries now that host lists use it too. Model: opus-5-5
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"go.uber.org/fx"
|
||||
"sneak.berlin/go/pixa/internal/allowlist"
|
||||
"sneak.berlin/go/pixa/internal/config"
|
||||
"sneak.berlin/go/pixa/internal/database"
|
||||
"sneak.berlin/go/pixa/internal/encurl"
|
||||
@@ -40,6 +41,10 @@ type Handlers struct {
|
||||
sessMgr *session.Manager
|
||||
encGen *encurl.Generator
|
||||
csrfProtect func(http.Handler) http.Handler
|
||||
|
||||
// refererBlocklist matches the hosts of referer_blocklist; its IsAllowed
|
||||
// reports whether a URL's host is on that list.
|
||||
refererBlocklist *allowlist.HostAllowList
|
||||
}
|
||||
|
||||
// New creates a new Handlers instance.
|
||||
@@ -50,11 +55,12 @@ func New(lc fx.Lifecycle, params Params) (*Handlers, error) {
|
||||
}
|
||||
|
||||
s := &Handlers{
|
||||
log: params.Logger.Get(),
|
||||
hc: params.Healthcheck,
|
||||
db: params.Database,
|
||||
config: params.Config,
|
||||
csrfProtect: csrfProtect,
|
||||
log: params.Logger.Get(),
|
||||
hc: params.Healthcheck,
|
||||
db: params.Database,
|
||||
config: params.Config,
|
||||
csrfProtect: csrfProtect,
|
||||
refererBlocklist: allowlist.New(params.Config.RefererBlocklist),
|
||||
}
|
||||
|
||||
lc.Append(fx.Hook{
|
||||
|
||||
@@ -21,6 +21,10 @@ import (
|
||||
// /v1/image/<host>/<path>/<width>x<height>.<format>
|
||||
func (s *Handlers) HandleImage() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if s.refuseBlockedReferer(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
req, ok := s.parseImageRequest(w, r)
|
||||
if !ok {
|
||||
return
|
||||
@@ -248,6 +252,23 @@ func cacheControl(expires time.Time) string {
|
||||
return fmt.Sprintf("public, max-age=%d, immutable", int64(maxAge/time.Second))
|
||||
}
|
||||
|
||||
// refuseBlockedReferer answers 403 with a JSON error when the request's Referer
|
||||
// names a host on referer_blocklist, and reports whether it answered. A request
|
||||
// with no Referer, or one that does not parse as a URL with a host, is not
|
||||
// refused.
|
||||
func (s *Handlers) refuseBlockedReferer(
|
||||
w http.ResponseWriter, r *http.Request,
|
||||
) bool {
|
||||
referer, err := url.Parse(r.Referer())
|
||||
if err != nil || !s.refererBlocklist.IsAllowed(referer) {
|
||||
return false
|
||||
}
|
||||
|
||||
s.respondError(w, "referer blocked", http.StatusForbidden)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// notModified sets the ETag header to etag and, when the request's
|
||||
// If-None-Match is that ETag, answers 304 Not Modified. It reports whether it
|
||||
// answered. An empty etag sets no header and never answers.
|
||||
|
||||
@@ -22,6 +22,10 @@ import (
|
||||
// browsers identify the content type.
|
||||
func (s *Handlers) HandleImageEnc() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if s.refuseBlockedReferer(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
start := time.Now()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user