118 lines
3.3 KiB
Lua
118 lines
3.3 KiB
Lua
-- 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
|