cachefp

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 24, 2026 License: MIT Imports: 19 Imported by: 0

README

cachefp

A Go implementation of supercookie — the favicon-cache browser fingerprinting technique — packaged as net/http middleware instead of a standalone demo site.

How it works

Modern browsers cache favicons the same way they cache any other resource. A server can tell whether a browser already has a given favicon URL cached: if it does, no request arrives; if it doesn't, one does. By generating a set of per-deployment probe URLs and selectively causing some of them to be cached (encoding a bit pattern) and later checking which ones are still cached (decoding it), a persistent identifier can be assigned to a visitor without using cookies, localStorage, or any other clearable storage mechanism.

cachefp wraps this in a middleware: it drives the whole probe sequence through a single, invisible background page (no visible redirects), and once an identifier is established it's cached in a regular cookie so every later request is just a cookie read — the expensive part only happens once per visitor.

Usage

package main

import (
	"fmt"
	"log"
	"net/http"

	"github.com/jim-ww/cachefp"
)

func main() {
	app := http.NewServeMux()
	app.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
		id, ok := cachefp.FromContext(r.Context())
		switch {
		case !ok:
			fmt.Fprintln(w, "no identifier yet for this request")
		case id.Identified:
			fmt.Fprintf(w, "visitor identified as %s (#%d)\n", id.Hash, id.ID)
		default:
			fmt.Fprintln(w, "visitor could not be identified")
		}
	})

	mw := cachefp.New(cachefp.Options{})
	log.Fatal(http.ListenAndServe(":8080", mw.Wrap(app)))
}

See cmd/example for a runnable version. cachefp.Options also lets you set a custom Store (default is a small JSON file), the mount prefix for the middleware's internal routes, cookie name/lifetime, and how many probe bits to generate.

Browser compatibility

Carried over from the original project's findings. Not independently re-verified per OS/version here; treat as a starting point, not a guarantee.

Current versions (as tested by the original project)
Browser Windows macOS Linux iOS Android Notes
Chrome (v111.0) ?
Safari (v14.0)
Edge (v87.0)
Firefox (v86.0) Fingerprint differs in incognito mode
Brave (v1.19.92)
Previous versions (vulnerable)
Browser Windows macOS Linux iOS Android
Brave (v1.14.0)
Firefox (< v84.0)

Current Chromium reliably survives a cookie clear as long as the browser cache itself isn't cleared and the session isn't torn down. Firefox-family browsers are unreliable for this technique because Firefox stores favicons through its own "Places" service rather than the standard HTTP cache, independent of any hardening.

Scalability & performance

By varying the number of bits — which corresponds to the number of probe routes — this technique can be scaled almost arbitrarily. It can distinguish 2^N unique visitors, where N is the number of probe routes walked on the client side. The time taken for the write/read pass grows with N; keeping N no larger than needed for the current visitor count keeps that pass short.

cachefp follows the same idea but doesn't require a fixed N up front: like the original, each visitor's read/write pass only walks ceil(log2(index)) + 1 probe routes (where index is the number of visitors assigned so far), not the full Options.ProbeBits (default 32) — so the pass stays short early on and only grows as more visitors get enrolled, while ProbeBits remains the hard ceiling (2^32-1 identifiers) on how large the deployment can ever grow.

Disclaimer

Provided for educational and security-research purposes. You are responsible for complying with applicable law (GDPR/ePrivacy, etc.) and for obtaining consent before identifying real users with it.

Documentation

Index

Constants

View Source
const DefaultProbeBits = 32

ProbeBits controls how many probe routes are generated per deployment; 2^ProbeBits-1 unique identifiers are possible. 32 is a sane default and matches a native machine word on most platforms.

Variables

This section is empty.

Functions

This section is empty.

Types

type Identifier

type Identifier struct {
	// ID is the raw decoded value. Only meaningful when Identified is true.
	ID uint64
	// Hash is a short, human-readable rendering of ID.
	Hash string
	// Identified reports whether the visitor's browser could be fingerprinted
	// via the cache probe. False means identification was attempted but the
	// browser didn't yield a usable signal (e.g. cache isolation, private
	// browsing, or repeated failures past MaxAttempts).
	Identified bool
}

Identifier is the result of a successful (or exhausted) identification pass.

func FromContext

func FromContext(ctx context.Context) (Identifier, bool)

FromContext returns the Identifier attached to the request context by the middleware, if any. It is absent for non-GET requests and for requests served before the middleware has finished a probe pass.

type InMemoryStore added in v0.1.1

type InMemoryStore sync.Map

func (*InMemoryStore) Get added in v0.1.1

func (s *InMemoryStore) Get(key string) (any, bool)

func (*InMemoryStore) Set added in v0.1.1

func (s *InMemoryStore) Set(key string, value any)

type JSONStore

type JSONStore struct {
	// contains filtered or unexported fields
}

JSONStore is the default Store: it persists state as a small JSON file.

func NewJSONStore

func NewJSONStore(path string) *JSONStore

NewJSONStore opens (or creates) the JSON file at path as a Store.

func (*JSONStore) Get

func (s *JSONStore) Get(key string) (any, bool)

func (*JSONStore) Set

func (s *JSONStore) Set(key string, value any)

type Middleware

type Middleware struct {
	// contains filtered or unexported fields
}

Middleware identifies visitors via cache probing and exposes the result through FromContext.

func New

func New(opts Options) *Middleware

New builds a Middleware. It reads (or creates) its persistent state immediately, via Options.Store if set, or a JSONStore at Options.StoragePath otherwise.

func (*Middleware) Wrap

func (m *Middleware) Wrap(next http.Handler) http.Handler

Wrap returns an http.Handler that identifies visitors before delegating to next. GET requests without a resolved identifier are sent to the background probe page and, once identification finishes (or gives up), navigated back to the URL they originally requested. Non-GET requests are passed through with no Identifier in context.

type Options

type Options struct {
	// MountPrefix is the path prefix under which the middleware serves its
	// internal probe routes. Must not collide with routes in the wrapped
	// application. Defaults to "/_cachefp".
	MountPrefix string
	// CookieName is where the resolved identifier is stored once known.
	// Defaults to "cfpid".
	CookieName string
	// CookieMaxAge controls how long an established identifier cookie lasts.
	// Defaults to 1 year.
	CookieMaxAge time.Duration
	// Store holds the small amount of persistent state (the per-deployment
	// cache ID and next write index). Defaults to a JSONStore at
	// StoragePath; set Store directly to use a different backend (a
	// database row, a KV store, etc.) instead of a JSON file.
	Store Store
	// StoragePath is where the default JSONStore keeps its file. Ignored if
	// Store is set. Defaults to "cachefp_data.json".
	StoragePath string
	// ProbeBits sets the number of probe routes; see DefaultProbeBits.
	ProbeBits int
	// MaxAttempts caps how many write/read passes are made before giving up
	// on a visitor and marking them non-identifiable, to avoid looping
	// forever for browsers that don't exhibit the caching behavior this
	// technique relies on. Defaults to 2.
	MaxAttempts int
	// Logf, if set, receives diagnostic log lines. Defaults to log.Printf.
	Logf func(format string, args ...any)
}

Options configures a Middleware.

type Store

type Store interface {
	Get(key string) (value any, ok bool)
	Set(key string, value any)
}

Store persists the middleware's small amount of durable state: the per-deployment cache ID and the next write index. Both must survive process restarts, or previously-cached probe URLs in visitors' browsers stop matching anything the server expects.

Get/Set values are limited to what encoding/json's default decoding produces (so implementations backed by JSON, like JSONStore, round-trip cleanly): strings and float64 for the numeric index. A custom Store may use a real integer type internally as long as GetUint64 converts it.

Directories

Path Synopsis
cmd
example command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL