-- Site-wide uberban checking module -- Blocks uberbanned fingerprints from accessing any endpoint local redis = require "resty.redis" local fingerprint = require "fingerprint" local _M = {} -- Cache for ban status (5 second TTL to reduce Redis load) local uberban_cache = ngx.shared.uberban_cache local CACHE_TTL = 5 -- seconds -- Redis password cached at module load time local REDIS_PASSWORD = os.getenv("REDIS_PASS") -- Get Redis connection (same pattern as redis_helper.lua) local function get_redis_connection() local red = redis:new() red:set_timeouts(1000, 1000, 1000) -- connect, send, read timeout in ms local host = "redis" local port = tonumber(os.getenv("REDIS_PORT")) or 6379 local ok, err = red:connect(host, port) if not ok then ngx.log(ngx.ERR, "[uberban] Failed to connect to Redis: ", err) return nil end -- Authenticate if password is set if REDIS_PASSWORD and REDIS_PASSWORD ~= "" then local res, err = red:auth(REDIS_PASSWORD) if not res then ngx.log(ngx.ERR, "[uberban] Failed to authenticate to Redis: ", err) return nil end end -- Select database 1 (where chat-service stores fingerprint bans) local db = tonumber(os.getenv("REDIS_DB")) or 1 local res, err = red:select(db) if not res then ngx.log(ngx.ERR, "[uberban] Failed to select Redis db ", db, ": ", err) return nil end return red end local function close_redis_connection(red) local ok, err = red:set_keepalive(10000, 100) if not ok then ngx.log(ngx.ERR, "[uberban] Failed to set keepalive: ", err) end end -- Check if a fingerprint is uberbanned -- Returns true if banned, false if not banned function _M.is_banned(fp) if not fp then return false end -- Check cache first if uberban_cache then local cached = uberban_cache:get(fp) if cached ~= nil then return cached == "1" end end -- Query Redis local red = get_redis_connection() if not red then -- SECURITY FIX: Fail closed - if Redis is unavailable, deny access -- This prevents banned users from accessing the system during outages ngx.log(ngx.ERR, "[uberban] Redis unavailable - blocking request (fail closed)") return true end -- Check if fingerprint is in the uberbanned set local res, err = red:sismember("chat:banned:fingerprints", fp) close_redis_connection(red) if not res then ngx.log(ngx.ERR, "[uberban] Redis SISMEMBER failed: ", err) return false end local is_banned = (res == 1) -- Cache the result if uberban_cache then uberban_cache:set(fp, is_banned and "1" or "0", CACHE_TTL) end return is_banned end -- Check fingerprint and block if uberbanned -- Call this in access_by_lua_block function _M.check_and_block() -- Generate server-side fingerprint local fp = fingerprint.generate() if not fp then return -- Can't generate fingerprint, allow request end -- Check if banned if _M.is_banned(fp) then ngx.status = ngx.HTTP_FORBIDDEN ngx.header["Content-Type"] = "application/json" ngx.say('{"error": "Access denied", "banned": true}') return ngx.exit(ngx.HTTP_FORBIDDEN) end end return _M