bunkerweb 1.4.0
This commit is contained in:
375
core/antibot/antibot.lua
Normal file
375
core/antibot/antibot.lua
Normal file
@@ -0,0 +1,375 @@
|
||||
local _M = {}
|
||||
_M.__index = _M
|
||||
|
||||
local utils = require "utils"
|
||||
local datastore = require "datastore"
|
||||
local logger = require "logger"
|
||||
local cjson = require "cjson"
|
||||
local session = require "resty.session"
|
||||
local captcha = require "antibot.captcha"
|
||||
local base64 = require "base64"
|
||||
local sha256 = require "resty.sha256"
|
||||
local str = require "resty.string"
|
||||
local http = require "resty.http"
|
||||
|
||||
function _M.new()
|
||||
local self = setmetatable({}, _M)
|
||||
return self, nil
|
||||
end
|
||||
|
||||
function _M:init()
|
||||
-- Check if init is needed
|
||||
local init_needed, err = utils.has_not_variable("USE_ANTIBOT", "no")
|
||||
if init_needed == nil then
|
||||
return false, err
|
||||
end
|
||||
if not init_needed then
|
||||
return true, "no service uses Antibot, skipping init"
|
||||
end
|
||||
-- Load templates
|
||||
local templates = {}
|
||||
for i, template in ipairs({"javascript", "captcha", "recaptcha", "hcaptcha"}) do
|
||||
local f, err = io.open("/opt/bunkerweb/core/antibot/files/" .. template .. ".html")
|
||||
if not f then
|
||||
return false, "error while loading " .. template .. ".html : " .. err
|
||||
end
|
||||
templates[template] = f:read("*all")
|
||||
f:close()
|
||||
end
|
||||
local ok, err = datastore:set("plugin_antibot_templates", cjson.encode(templates))
|
||||
if not ok then
|
||||
return false, "can't save templates to datastore : " .. err
|
||||
end
|
||||
return true, "success"
|
||||
end
|
||||
|
||||
function _M:access()
|
||||
-- Check if access is needed
|
||||
local antibot, err = utils.get_variable("USE_ANTIBOT")
|
||||
if antibot == nil then
|
||||
return false, err, nil, nil
|
||||
end
|
||||
if antibot == "no" then
|
||||
return true, "Antibot not activated", nil, nil
|
||||
end
|
||||
|
||||
-- Get challenge URI
|
||||
local challenge_uri, err = utils.get_variable("ANTIBOT_URI")
|
||||
if not challenge_uri then
|
||||
return false, "can't get Antibot URI from datastore : " .. err, nil, nil
|
||||
end
|
||||
|
||||
-- Don't go further if client resolved the challenge
|
||||
local resolved, err, original_uri = self:challenge_resolved(antibot)
|
||||
if resolved == nil then
|
||||
return false, "can't check if challenge is resolved : " .. err, nil, nil
|
||||
end
|
||||
if resolved then
|
||||
if ngx.var.uri == challenge_uri then
|
||||
return true, "client already resolved the challenge", true, ngx.redirect(original_uri)
|
||||
end
|
||||
return true, "client already resolved the challenge", nil, nil
|
||||
end
|
||||
|
||||
-- Redirect to challenge page
|
||||
if ngx.var.uri ~= challenge_uri then
|
||||
local ok, err = self:prepare_challenge(antibot, challenge_uri)
|
||||
if not ok then
|
||||
return false, "can't prepare challenge : " .. err, true, ngx.HTTP_INTERNAL_SERVER_ERROR
|
||||
end
|
||||
return true, "redirecting client to the challenge uri", true, ngx.redirect(challenge_uri)
|
||||
end
|
||||
|
||||
-- Display challenge
|
||||
if ngx.var.request_method == "GET" then
|
||||
local ok, err = self:display_challenge(antibot, challenge_uri)
|
||||
if not ok then
|
||||
if err == "can't open session" then
|
||||
local ok, err = self:prepare_challenge(antibot, challenge_uri)
|
||||
if not ok then
|
||||
return false, "can't prepare challenge : " .. err, true, ngx.HTTP_INTERNAL_SERVER_ERROR
|
||||
end
|
||||
return true, "redirecting client to the challenge uri", true, ngx.redirect(challenge_uri)
|
||||
end
|
||||
return false, "display challenge error : " .. err, true, ngx.HTTP_INTERNAL_SERVER_ERROR
|
||||
end
|
||||
return true, "displaying challenge to client", true, ngx.HTTP_OK
|
||||
end
|
||||
|
||||
-- Check challenge
|
||||
if ngx.var.request_method == "POST" then
|
||||
local ok, err, redirect = self:check_challenge(antibot)
|
||||
if ok == nil then
|
||||
if err == "can't open session" then
|
||||
local ok, err = self:prepare_challenge(antibot, challenge_uri)
|
||||
if not ok then
|
||||
return false, "can't prepare challenge : " .. err, true, ngx.HTTP_INTERNAL_SERVER_ERROR
|
||||
end
|
||||
return true, "redirecting client to the challenge uri", true, ngx.redirect(challenge_uri)
|
||||
end
|
||||
return false, "check challenge error : " .. err, true, ngx.HTTP_INTERNAL_SERVER_ERROR
|
||||
end
|
||||
if redirect then
|
||||
return true, "check challenge redirect : " .. redirect, true, ngx.redirect(redirect)
|
||||
end
|
||||
local ok, err = self:display_challenge(antibot)
|
||||
if not ok then
|
||||
if err == "can't open session" then
|
||||
local ok, err = self:prepare_challenge(antibot, challenge_uri)
|
||||
if not ok then
|
||||
return false, "can't prepare challenge : " .. err, true, ngx.HTTP_INTERNAL_SERVER_ERROR
|
||||
end
|
||||
return true, "redirecting client to the challenge uri", true, ngx.redirect(challenge_uri)
|
||||
end
|
||||
return false, "display challenge error : " .. err, true, ngx.HTTP_INTERNAL_SERVER_ERROR
|
||||
end
|
||||
return true, "displaying challenge to client", true, ngx.HTTP_OK
|
||||
end
|
||||
|
||||
-- Method is suspicious, let's deny the request
|
||||
return true, "unsupported HTTP method for Antibot", true, ngx.HTTP_FORBIDDEN
|
||||
|
||||
end
|
||||
|
||||
function _M:challenge_resolved(antibot)
|
||||
local chall_session, present, reason = session.open()
|
||||
if present and chall_session.data.resolved and chall_session.data.type == antibot then
|
||||
return true, "challenge " .. antibot .. " resolved", chall_session.data.original_uri
|
||||
end
|
||||
return false, "challenge " .. antibot .. " not resolved", nil
|
||||
end
|
||||
|
||||
function _M:prepare_challenge(antibot, challenge_uri)
|
||||
local chall_session, present, reason = session.open()
|
||||
if not present then
|
||||
local chall_session, present, reason = chall_session:start()
|
||||
if not chall_session then
|
||||
return false, "can't start session", nil
|
||||
end
|
||||
chall_session.data.type = antibot
|
||||
chall_session.data.resolved = false
|
||||
if ngx.var.request_uri == challenge_uri then
|
||||
chall_session.data.original_uri = "/"
|
||||
else
|
||||
chall_session.data.original_uri = ngx.var.request_uri
|
||||
end
|
||||
if antibot == "cookie" then
|
||||
chall_session.data.resolved = true
|
||||
end
|
||||
local saved, err = chall_session:save()
|
||||
if not saved then
|
||||
return false, "error while saving session : " .. err
|
||||
end
|
||||
end
|
||||
return true, antibot .. " challenge prepared"
|
||||
end
|
||||
|
||||
function _M:display_challenge(antibot, challenge_uri)
|
||||
-- Open session
|
||||
local chall_session, present, reason = session.open()
|
||||
if not present then
|
||||
return false, "can't open session"
|
||||
end
|
||||
|
||||
-- Check if session type is equal to antibot type
|
||||
if antibot ~= chall_session.data.type then
|
||||
return false, "session type is different from antibot type"
|
||||
end
|
||||
|
||||
-- Compute challenges
|
||||
if antibot == "javascript" then
|
||||
chall_session:start()
|
||||
chall_session.data.random = utils.rand(20)
|
||||
chall_session:save()
|
||||
elseif antibot == "captcha" then
|
||||
chall_session:start()
|
||||
local chall_captcha = captcha.new()
|
||||
chall_captcha:font("/opt/bunkerweb/core/antibot/files/font.ttf")
|
||||
chall_captcha:generate()
|
||||
chall_session.data.image = base64.encode(chall_captcha:jpegStr(70))
|
||||
chall_session.data.text = chall_captcha:getStr()
|
||||
chall_session:save()
|
||||
end
|
||||
|
||||
-- Load HTML templates
|
||||
local str_templates, err = datastore:get("plugin_antibot_templates")
|
||||
if not str_templates then
|
||||
return false, "can't get templates from datastore : " .. err
|
||||
end
|
||||
local templates = cjson.decode(str_templates)
|
||||
|
||||
local html = ""
|
||||
|
||||
-- Javascript case
|
||||
if antibot == "javascript" then
|
||||
html = templates.javascript:format(challenge_uri, chall_session.data.random)
|
||||
end
|
||||
|
||||
-- Captcha case
|
||||
if antibot == "captcha" then
|
||||
html = templates.captcha:format(challenge_uri, chall_session.data.image)
|
||||
end
|
||||
|
||||
-- reCAPTCHA case
|
||||
if antibot == "recaptcha" then
|
||||
local recaptcha_sitekey, err = utils.get_variable("ANTIBOT_RECAPTCHA_SITEKEY")
|
||||
if not recaptcha_sitekey then
|
||||
return false, "can't get reCAPTCHA sitekey variable : " .. err
|
||||
end
|
||||
html = templates.recaptcha:format(recaptcha_sitekey, challenge_uri, recaptcha_sitekey)
|
||||
end
|
||||
|
||||
-- hCaptcha case
|
||||
if antibot == "hcaptcha" then
|
||||
local hcaptcha_sitekey, err = utils.get_variable("ANTIBOT_HCAPTCHA_SITEKEY")
|
||||
if not hcaptcha_sitekey then
|
||||
return false, "can't get hCaptcha sitekey variable : " .. err
|
||||
end
|
||||
html = templates.hcaptcha:format(challenge_uri, hcaptcha_sitekey)
|
||||
end
|
||||
|
||||
ngx.header["Content-Type"] = "text/html"
|
||||
ngx.say(html)
|
||||
|
||||
return true, "displayed challenge"
|
||||
|
||||
end
|
||||
|
||||
function _M:check_challenge(antibot)
|
||||
-- Open session
|
||||
local chall_session, present, reason = session.open()
|
||||
if not present then
|
||||
return nil, "can't open session", nil
|
||||
end
|
||||
|
||||
-- Check if session type is equal to antibot type
|
||||
if antibot ~= chall_session.data.type then
|
||||
return nil, "session type is different from antibot type", nil
|
||||
end
|
||||
|
||||
local resolved = false
|
||||
local err = ""
|
||||
local redirect = nil
|
||||
|
||||
-- Javascript case
|
||||
if antibot == "javascript" then
|
||||
ngx.req.read_body()
|
||||
local args, err = ngx.req.get_post_args(1)
|
||||
if err == "truncated" or not args or not args["challenge"] then
|
||||
return false, "missing challenge arg", nil
|
||||
end
|
||||
local hash = sha256:new()
|
||||
hash:update(chall_session.data.random .. args["challenge"])
|
||||
local digest = hash:final()
|
||||
resolved = str.to_hex(digest):find("^0000") ~= nil
|
||||
if not resolved then
|
||||
return false, "wrong value", nil
|
||||
end
|
||||
chall_session:start()
|
||||
chall_session.data.resolved = true
|
||||
chall_session:save()
|
||||
return true, "resolved", chall_session.data.original_uri
|
||||
end
|
||||
|
||||
-- Captcha case
|
||||
if antibot == "captcha" then
|
||||
ngx.req.read_body()
|
||||
local args, err = ngx.req.get_post_args(1)
|
||||
if err == "truncated" or not args or not args["captcha"] then
|
||||
return false, "missing challenge arg", nil
|
||||
end
|
||||
if chall_session.data.text ~= args["captcha"] then
|
||||
return false, "wrong value", nil
|
||||
end
|
||||
chall_session:start()
|
||||
chall_session.data.resolved = true
|
||||
chall_session:save()
|
||||
return true, "resolved", chall_session.data.original_uri
|
||||
end
|
||||
|
||||
-- reCAPTCHA case
|
||||
if antibot == "recaptcha" then
|
||||
ngx.req.read_body()
|
||||
local args, err = ngx.req.get_post_args(1)
|
||||
if err == "truncated" or not args or not args["token"] then
|
||||
return false, "missing challenge arg", nil
|
||||
end
|
||||
local recaptcha_secret, err = utils.get_variable("ANTIBOT_RECAPTCHA_SECRET")
|
||||
if not recaptcha_secret then
|
||||
return nil, "can't get reCAPTCHA secret variable : " .. err, nil
|
||||
end
|
||||
local httpc, err = http.new()
|
||||
if not httpc then
|
||||
return false, "can't instantiate http object : " .. err, nil, nil
|
||||
end
|
||||
local res, err = httpc:request_uri("https://www.google.com/recaptcha/api/siteverify", {
|
||||
method = "POST",
|
||||
body = "secret=" .. recaptcha_secret .. "&response=" .. args["token"] .. "&remoteip=" .. ngx.var.remote_addr,
|
||||
headers = {
|
||||
["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
}
|
||||
})
|
||||
httpc:close()
|
||||
if not res then
|
||||
return nil, "can't send request to reCAPTCHA API : " .. err, nil
|
||||
end
|
||||
local ok, data = pcall(cjson.decode, res.body)
|
||||
if not ok then
|
||||
return nil, "error while decoding JSON from reCAPTCHA API : " .. data, nil
|
||||
end
|
||||
local recaptcha_score, err = utils.get_variable("ANTIBOT_RECAPTCHA_SCORE")
|
||||
if not recaptcha_score then
|
||||
return nil, "can't get reCAPTCHA score variable : " .. err, nil
|
||||
end
|
||||
if not data.success or data.score < tonumber(recaptcha_score) then
|
||||
return false, "client failed challenge with score " .. tostring(data.score), nil
|
||||
end
|
||||
chall_session:start()
|
||||
chall_session.data.resolved = true
|
||||
chall_session:save()
|
||||
return true, "resolved", chall_session.data.original_uri
|
||||
end
|
||||
|
||||
-- hCaptcha case
|
||||
if antibot == "hcaptcha" then
|
||||
ngx.req.read_body()
|
||||
local args, err = ngx.req.get_post_args(1)
|
||||
if err == "truncated" or not args or not args["token"] then
|
||||
return false, "missing challenge arg", nil
|
||||
end
|
||||
local hcaptcha_secret, err = utils.get_variable("ANTIBOT_HCAPTCHA_SECRET")
|
||||
if not hcaptcha_secret then
|
||||
return nil, "can't get hCaptcha secret variable : " .. err, nil
|
||||
end
|
||||
local httpc, err = http.new()
|
||||
if not httpc then
|
||||
return false, "can't instantiate http object : " .. err, nil, nil
|
||||
end
|
||||
local res, err = httpc:request_uri("https://hcaptcha.com/siteverify", {
|
||||
method = "POST",
|
||||
body = "secret=" .. hcaptcha_secret .. "&response=" .. args["token"] .. "&remoteip=" .. ngx.var.remote_addr,
|
||||
headers = {
|
||||
["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
}
|
||||
})
|
||||
httpc:close()
|
||||
if not res then
|
||||
return nil, "can't send request to hCaptcha API : " .. err, nil
|
||||
end
|
||||
local ok, data = pcall(cjson.decode, res.body)
|
||||
if not ok then
|
||||
return nil, "error while decoding JSON from hCaptcha API : " .. data, nil
|
||||
end
|
||||
if not data.success then
|
||||
return false, "client failed challenge", nil
|
||||
end
|
||||
chall_session:start()
|
||||
chall_session.data.resolved = true
|
||||
chall_session:save()
|
||||
return true, "resolved", chall_session.data.original_uri
|
||||
end
|
||||
|
||||
return nil, "unknown", nil
|
||||
end
|
||||
|
||||
return _M
|
||||
193
core/antibot/captcha.lua
Normal file
193
core/antibot/captcha.lua
Normal file
@@ -0,0 +1,193 @@
|
||||
-- Copyright startx <startx@plentyfact.org>
|
||||
-- Modifications copyright mrDoctorWho <mrdoctorwho@gmail.com>
|
||||
-- Published under the MIT license
|
||||
|
||||
local _M = {}
|
||||
|
||||
local gd = require 'gd'
|
||||
local logger = require "logger"
|
||||
|
||||
local mt = { __index = {} }
|
||||
|
||||
|
||||
function _M.new()
|
||||
local cap = {}
|
||||
local f = setmetatable({ cap = cap}, mt)
|
||||
return f
|
||||
end
|
||||
|
||||
|
||||
local function urandom()
|
||||
local seed = 1
|
||||
local devurandom = io.open("/dev/urandom", "r")
|
||||
local urandom = devurandom:read(32)
|
||||
devurandom:close()
|
||||
|
||||
for i=1,string.len(urandom) do
|
||||
local s = string.byte(urandom,i)
|
||||
seed = seed + s
|
||||
end
|
||||
return seed
|
||||
end
|
||||
|
||||
|
||||
local function random_char(length)
|
||||
local set, char, uid
|
||||
-- local set = [[1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ]]
|
||||
local set = [[abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ]]
|
||||
local captcha_t = {}
|
||||
|
||||
math.randomseed(urandom())
|
||||
|
||||
for c=1,length do
|
||||
local i = math.random(1, string.len(set))
|
||||
table.insert(captcha_t, string.sub(set,i,i))
|
||||
end
|
||||
|
||||
return captcha_t
|
||||
end
|
||||
|
||||
|
||||
local function random_angle()
|
||||
math.randomseed(urandom())
|
||||
return math.random(-20, 40)
|
||||
end
|
||||
|
||||
|
||||
local function scribble(w,h)
|
||||
math.randomseed(urandom())
|
||||
local x1 = math.random(5, w - 5)
|
||||
local x2 = math.random(5, w - 5)
|
||||
return x1, x2
|
||||
end
|
||||
|
||||
|
||||
function mt.__index:string(s)
|
||||
self.cap.string = s
|
||||
end
|
||||
|
||||
function mt.__index:scribble(n)
|
||||
self.cap.scribble = n or 20
|
||||
end
|
||||
|
||||
function mt.__index:length(l)
|
||||
self.cap.length = l
|
||||
end
|
||||
|
||||
|
||||
function mt.__index:bgcolor(r,g,b)
|
||||
self.cap.bgcolor = { r = r , g = g , b = b}
|
||||
end
|
||||
|
||||
function mt.__index:fgcolor(r,g,b)
|
||||
self.cap.fgcolor = { r = r , g = g , b = b}
|
||||
end
|
||||
|
||||
function mt.__index:line(line)
|
||||
self.cap.line = line
|
||||
end
|
||||
|
||||
|
||||
function mt.__index:font(font)
|
||||
self.cap.font = font
|
||||
end
|
||||
|
||||
|
||||
function mt.__index:generate()
|
||||
--local self.captcha = {}
|
||||
local captcha_t = {}
|
||||
|
||||
if not self.cap.string then
|
||||
if not self.cap.length then
|
||||
self.cap.length = 6
|
||||
end
|
||||
captcha_t = random_char(self.cap.length)
|
||||
self:string(table.concat(captcha_t))
|
||||
else
|
||||
for i=1, #self.cap.string do
|
||||
table.insert(captcha_t, string.sub(self.cap.string, i, i))
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
self.im = gd.createTrueColor(#captcha_t * 40, 45)
|
||||
local black = self.im:colorAllocate(0, 0, 0)
|
||||
local white = self.im:colorAllocate(255, 255, 255)
|
||||
local bgcolor
|
||||
if not self.cap.bgcolor then
|
||||
bgcolor = white
|
||||
else
|
||||
bgcolor = self.im:colorAllocate(self.cap.bgcolor.r , self.cap.bgcolor.g, self.cap.bgcolor.b )
|
||||
end
|
||||
|
||||
local fgcolor
|
||||
if not self.cap.fgcolor then
|
||||
fgcolor = black
|
||||
else
|
||||
fgcolor = self.im:colorAllocate(self.cap.fgcolor.r , self.cap.fgcolor.g, self.cap.fgcolor.b )
|
||||
end
|
||||
|
||||
self.im:filledRectangle(0, 0, #captcha_t * 40, 45, bgcolor)
|
||||
|
||||
local offset_left = 10
|
||||
|
||||
for i=1, #captcha_t do
|
||||
local angle = random_angle()
|
||||
local llx, lly, lrx, lry, urx, ury, ulx, uly = self.im:stringFT(fgcolor, self.cap.font, 25, math.rad(angle), offset_left, 35, captcha_t[i])
|
||||
self.im:polygon({ {llx, lly}, {lrx, lry}, {urx, ury}, {ulx, uly} }, bgcolor)
|
||||
offset_left = offset_left + 40
|
||||
end
|
||||
|
||||
if self.cap.line then
|
||||
self.im:line(10, 10, ( #captcha_t * 40 ) - 10 , 40, fgcolor)
|
||||
self.im:line(11, 11, ( #captcha_t * 40 ) - 11 , 41, fgcolor)
|
||||
self.im:line(12, 12, ( #captcha_t * 40 ) - 12 , 42, fgcolor)
|
||||
end
|
||||
|
||||
|
||||
if self.cap.scribble then
|
||||
for i=1,self.cap.scribble do
|
||||
local x1,x2 = scribble( #captcha_t * 40 , 45 )
|
||||
self.im:line(x1, 5, x2, 40, fgcolor)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Perhaps it's not the best solution
|
||||
-- Writes the generated image to a jpeg file
|
||||
function mt.__index:jpeg(outfile, quality)
|
||||
self.im:jpeg(outfile, quality)
|
||||
end
|
||||
|
||||
-- Writes the generated image to a png file
|
||||
function mt.__index:png(outfile)
|
||||
self.im:png(outfile)
|
||||
end
|
||||
|
||||
-- Allows to get the image data in PNG format
|
||||
function mt.__index:pngStr()
|
||||
return self.im:pngStr()
|
||||
end
|
||||
|
||||
-- Allows to get the image data in JPEG format
|
||||
function mt.__index:jpegStr(quality)
|
||||
return self.im:jpegStr(quality)
|
||||
end
|
||||
|
||||
-- Allows to get the image text
|
||||
function mt.__index:getStr()
|
||||
return self.cap.string
|
||||
end
|
||||
|
||||
-- Writes the image to a file
|
||||
function mt.__index:write(outfile, quality)
|
||||
if self.cap.string == nil then
|
||||
self:generate()
|
||||
end
|
||||
self:jpeg(outfile, quality)
|
||||
-- Compatibility
|
||||
return self:getStr()
|
||||
end
|
||||
|
||||
return _M
|
||||
9
core/antibot/confs/http/antibot.conf
Normal file
9
core/antibot/confs/http/antibot.conf
Normal file
@@ -0,0 +1,9 @@
|
||||
map "{{ ANTIBOT_SESSION_SECRET }}" $session_secret {
|
||||
default "{{ ANTIBOT_SESSION_SECRET }}";
|
||||
"random" "{{ random(32) }}";
|
||||
}
|
||||
|
||||
map "{{ ANTIBOT_SESSION_NAME }}" $session_name {
|
||||
default "{{ ANTIBOT_SESSION_NAME }}";
|
||||
"random" "{{ random(16) }}";
|
||||
}
|
||||
30
core/antibot/files/captcha.html
Normal file
30
core/antibot/files/captcha.html
Normal file
File diff suppressed because one or more lines are too long
BIN
core/antibot/files/font.ttf
Normal file
BIN
core/antibot/files/font.ttf
Normal file
Binary file not shown.
44
core/antibot/files/hcaptcha.html
Normal file
44
core/antibot/files/hcaptcha.html
Normal file
File diff suppressed because one or more lines are too long
261
core/antibot/files/javascript.html
Normal file
261
core/antibot/files/javascript.html
Normal file
File diff suppressed because one or more lines are too long
49
core/antibot/files/recaptcha.html
Normal file
49
core/antibot/files/recaptcha.html
Normal file
File diff suppressed because one or more lines are too long
27
core/antibot/files/template.html
Normal file
27
core/antibot/files/template.html
Normal file
File diff suppressed because one or more lines are too long
98
core/antibot/plugin.json
Normal file
98
core/antibot/plugin.json
Normal file
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"id": "antibot",
|
||||
"order": 4,
|
||||
"name": "Antibot",
|
||||
"description": "Bot detection by using a challenge.",
|
||||
"version": "0.1",
|
||||
"settings": {
|
||||
"USE_ANTIBOT": {
|
||||
"context": "multisite",
|
||||
"default": "no",
|
||||
"help": "Activate antibot feature.",
|
||||
"id": "use-antibot",
|
||||
"label": "Antibot challenge",
|
||||
"regex": "^(no|cookie|javascript|captcha|recaptcha|hcaptcha)$",
|
||||
"type": "select",
|
||||
"select": [
|
||||
"no",
|
||||
"cookie",
|
||||
"javascript",
|
||||
"captcha",
|
||||
"recaptcha",
|
||||
"hcaptcha"
|
||||
]
|
||||
},
|
||||
"ANTIBOT_URI": {
|
||||
"context": "multisite",
|
||||
"default": "/challenge",
|
||||
"help": "Unused URI that clients will be redirected to solve the challenge.",
|
||||
"id": "antibot-uri",
|
||||
"label": "Antibot URL",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"ANTIBOT_SESSION_SECRET": {
|
||||
"context": "global",
|
||||
"default": "random",
|
||||
"help": "Secret used to encrypt sessions variables for storing data related to challenges.",
|
||||
"id": "antibot-session-secret",
|
||||
"label": "Session secret",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"ANTIBOT_SESSION_NAME": {
|
||||
"context": "global",
|
||||
"default": "random",
|
||||
"help": "Name of the cookie used by the antibot feature.",
|
||||
"id": "antibot-session-name",
|
||||
"label": "Session name",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"ANTIBOT_RECAPTCHA_SCORE": {
|
||||
"context": "multisite",
|
||||
"default": "0.7",
|
||||
"help": "Minimum score required for reCAPTCHA challenge.",
|
||||
"id": "antibot-recaptcha-score",
|
||||
"label": "reCAPTCHA score",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"ANTIBOT_RECAPTCHA_SITEKEY": {
|
||||
"context": "multisite",
|
||||
"default": "",
|
||||
"help": "Sitekey for reCAPTCHA challenge.",
|
||||
"id": "antibot-recaptcha-sitekey",
|
||||
"label": "reCAPTCHA sitekey",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"ANTIBOT_RECAPTCHA_SECRET": {
|
||||
"context": "multisite",
|
||||
"default": "",
|
||||
"help": "Secret for reCAPTCHA challenge.",
|
||||
"id": "antibot-recaptcha-secret",
|
||||
"label": "reCAPTCHA secret",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"ANTIBOT_HCAPTCHA_SITEKEY": {
|
||||
"context": "multisite",
|
||||
"default": "",
|
||||
"help": "Sitekey for hCaptcha challenge.",
|
||||
"id": "antibot-hcaptcha-sitekey",
|
||||
"label": "hCaptcha sitekey",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"ANTIBOT_HCAPTCHA_SECRET": {
|
||||
"context": "multisite",
|
||||
"default": "",
|
||||
"help": "Secret for hCaptcha challenge.",
|
||||
"id": "antibot-hcaptcha-secret",
|
||||
"label": "hCaptcha secret",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
11
core/authbasic/confs/server-http/auth-basic.conf
Normal file
11
core/authbasic/confs/server-http/auth-basic.conf
Normal file
@@ -0,0 +1,11 @@
|
||||
{% if USE_AUTH_BASIC == "yes" +%}
|
||||
{% if AUTH_BASIC_LOCATION == "sitewide" %}
|
||||
auth_basic "{{ AUTH_BASIC_TEXT }}";
|
||||
auth_basic_user_file {{ NGINX_PREFIX }}server-http/htpasswd;
|
||||
{% else %}
|
||||
location {{ AUTH_BASIC_LOCATION }} {
|
||||
auth_basic "{{ AUTH_BASIC_TEXT }}";
|
||||
auth_basic_user_file {{ NGINX_PREFIX }}server-http/htpasswd;
|
||||
}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
4
core/authbasic/confs/server-http/htpasswd
Normal file
4
core/authbasic/confs/server-http/htpasswd
Normal file
@@ -0,0 +1,4 @@
|
||||
{% set crypt = import('crypt') %}
|
||||
{% if USE_AUTH_BASIC == "yes" %}
|
||||
{{ AUTH_BASIC_USER }}:{{ crypt.crypt(AUTH_BASIC_PASSWORD, crypt.mksalt(crypt.METHOD_SHA512)) }}
|
||||
{% endif %}
|
||||
54
core/authbasic/plugin.json
Normal file
54
core/authbasic/plugin.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"id": "authbasic",
|
||||
"order": 999,
|
||||
"name": "Auth basic",
|
||||
"description": "Enforce login before accessing a resource or the whole site using HTTP basic auth method.",
|
||||
"version": "0.1",
|
||||
"settings": {
|
||||
"USE_AUTH_BASIC": {
|
||||
"context": "multisite",
|
||||
"default": "no",
|
||||
"help": "Use HTTP basic auth",
|
||||
"id": "use-auth-basic",
|
||||
"label": "Use HTTP basic auth",
|
||||
"regex": "^(yes|no)$",
|
||||
"type": "check"
|
||||
},
|
||||
"AUTH_BASIC_LOCATION": {
|
||||
"context": "multisite",
|
||||
"default": "sitewide",
|
||||
"help": "URL of the protected resource or sitewide value.",
|
||||
"id": "auth-basic-location",
|
||||
"label": "Location",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"AUTH_BASIC_USER": {
|
||||
"context": "multisite",
|
||||
"default": "changeme",
|
||||
"help": "Username",
|
||||
"id": "auth-basic-user",
|
||||
"label": "Username",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"AUTH_BASIC_PASSWORD": {
|
||||
"context": "multisite",
|
||||
"default": "changeme",
|
||||
"help": "Password",
|
||||
"id": "auth-basic-password",
|
||||
"label": "Password",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"AUTH_BASIC_TEXT": {
|
||||
"context": "multisite",
|
||||
"default": "Restricted area",
|
||||
"help": "Text to display",
|
||||
"id": "auth-basic-text",
|
||||
"label": "Text",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
72
core/badbehavior/badbehavior.lua
Normal file
72
core/badbehavior/badbehavior.lua
Normal file
@@ -0,0 +1,72 @@
|
||||
local _M = {}
|
||||
_M.__index = _M
|
||||
|
||||
local utils = require "utils"
|
||||
local datastore = require "datastore"
|
||||
local logger = require "logger"
|
||||
local cjson = require "cjson"
|
||||
|
||||
function _M.new()
|
||||
local self = setmetatable({}, _M)
|
||||
return self, nil
|
||||
end
|
||||
|
||||
function _M:log()
|
||||
self.use = utils.get_variable("USE_BAD_BEHAVIOR")
|
||||
self.ban_time = utils.get_variable("BAD_BEHAVIOR_BAN_TIME")
|
||||
self.status_codes = utils.get_variable("BAD_BEHAVIOR_STATUS_CODES")
|
||||
self.threshold = utils.get_variable("BAD_BEHAVIOR_THRESHOLD")
|
||||
self.count_time = utils.get_variable("BAD_BEHAVIOR_COUNT_TIME")
|
||||
if self.use ~= "yes" then
|
||||
return true, "bad behavior not activated"
|
||||
end
|
||||
if not self.status_codes:match(tostring(ngx.status)) then
|
||||
return true, "not increasing counter"
|
||||
end
|
||||
local count, err = datastore:get("plugin_badbehavior_count_" .. ngx.var.remote_addr)
|
||||
if not count and err ~= "not found" then
|
||||
return false, "can't get counts from the datastore : " .. err
|
||||
end
|
||||
local new_count = 1
|
||||
if count ~= nil then
|
||||
new_count = count + 1
|
||||
end
|
||||
local ok, err = datastore:set("plugin_badbehavior_count_" .. ngx.var.remote_addr, new_count)
|
||||
if not ok then
|
||||
return false, "can't save counts to the datastore : " .. err
|
||||
end
|
||||
local function decrease_callback(premature, ip)
|
||||
local count, err = datastore:get("plugin_badbehavior_count_" .. ip)
|
||||
if err then
|
||||
logger.log(ngx.ERR, "BAD-BEHAVIOR", "(decrease_callback) Can't get counts from the datastore : " .. err)
|
||||
return
|
||||
end
|
||||
if not count then
|
||||
logger.log(ngx.ERR, "BAD-BEHAVIOR", "(decrease_callback) Count is null")
|
||||
return
|
||||
end
|
||||
local new_count = count - 1
|
||||
if new_count <= 0 then
|
||||
datastore:delete("plugin_badbehavior_count_" .. ip)
|
||||
return
|
||||
end
|
||||
local ok, err = datastore:set("plugin_badbehavior_count_" .. ip, new_count)
|
||||
if not ok then
|
||||
logger.log(ngx.ERR, "BAD-BEHAVIOR", "(decrease_callback) Can't save counts to the datastore : " .. err)
|
||||
end
|
||||
end
|
||||
local hdr, err = ngx.timer.at(tonumber(self.count_time), decrease_callback, ngx.var.remote_addr)
|
||||
if not ok then
|
||||
return false, "can't create decrease timer : " .. err
|
||||
end
|
||||
if new_count > tonumber(self.threshold) then
|
||||
local ok, err = datastore:set("bans_ip_" .. ngx.var.remote_addr, "bad behavior", tonumber(self.ban_time))
|
||||
if not ok then
|
||||
return false, "can't save ban to the datastore : " .. err
|
||||
end
|
||||
logger.log(ngx.WARN, "BAD-BEHAVIOR", "IP " .. ngx.var.remote_addr .. " is banned for " .. tostring(self.ban_time) .. "s (" .. tostring(new_count) .. "/" .. tostring(self.threshold) .. ")")
|
||||
end
|
||||
return true, "success"
|
||||
end
|
||||
|
||||
return _M
|
||||
54
core/badbehavior/plugin.json
Normal file
54
core/badbehavior/plugin.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"id": "badbehavior",
|
||||
"order": 2,
|
||||
"name": "Bad behavior",
|
||||
"description": "Ban IP generating too much 'bad' HTTP status code in a period of time.",
|
||||
"version": "0.1",
|
||||
"settings": {
|
||||
"USE_BAD_BEHAVIOR": {
|
||||
"context": "multisite",
|
||||
"default": "yes",
|
||||
"help": "Activate Bad behavior feature.",
|
||||
"id": "use-bad-behavior",
|
||||
"label": "Activate bad behavior",
|
||||
"regex": "^(yes|no)$",
|
||||
"type": "check"
|
||||
},
|
||||
"BAD_BEHAVIOR_STATUS_CODES": {
|
||||
"context": "multisite",
|
||||
"default": "400 401 403 404 405 429 444",
|
||||
"help": "List of HTTP status codes considered as 'bad'.",
|
||||
"id": "bad-behavior-status-code",
|
||||
"label": "Bad status codes",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"BAD_BEHAVIOR_BAN_TIME": {
|
||||
"context": "multisite",
|
||||
"default": "86400",
|
||||
"help": "The duration time (in seconds) of a ban when the corresponding IP has reached the threshold.",
|
||||
"id": "bad-behavior-ban-time",
|
||||
"label": "Ban duration (in seconds)",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"BAD_BEHAVIOR_THRESHOLD": {
|
||||
"context": "multisite",
|
||||
"default": "10",
|
||||
"help": "Maximum number of 'bad' HTTP status codes within the period of time before IP is banned.",
|
||||
"id": "bad-behavior-threshold",
|
||||
"label": "Threshold",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"BAD_BEHAVIOR_COUNT_TIME": {
|
||||
"context": "multisite",
|
||||
"default": "60",
|
||||
"help": "Period of time where we count 'bad' HTTP status codes.",
|
||||
"id": "bad-behavior-period",
|
||||
"label": "Period (in seconds)",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
245
core/blacklist/blacklist.lua
Normal file
245
core/blacklist/blacklist.lua
Normal file
@@ -0,0 +1,245 @@
|
||||
local _M = {}
|
||||
_M.__index = _M
|
||||
|
||||
local utils = require "utils"
|
||||
local datastore = require "datastore"
|
||||
local logger = require "logger"
|
||||
local cjson = require "cjson"
|
||||
local ipmatcher = require "resty.ipmatcher"
|
||||
|
||||
function _M.new()
|
||||
local self = setmetatable({}, _M)
|
||||
return self, nil
|
||||
end
|
||||
|
||||
function _M:init()
|
||||
-- Check if init is needed
|
||||
local init_needed, err = utils.has_variable("USE_BLACKLIST", "yes")
|
||||
if init_needed == nil then
|
||||
return false, err
|
||||
end
|
||||
if not init_needed then
|
||||
return true, "no service uses Blacklist, skipping init"
|
||||
end
|
||||
-- Read blacklists
|
||||
local blacklists = {
|
||||
["IP"] = {},
|
||||
["RDNS"] = {},
|
||||
["ASN"] = {},
|
||||
["USER_AGENT"] = {},
|
||||
["URI"] = {}
|
||||
}
|
||||
local i = 0
|
||||
for kind, _ in pairs(blacklists) do
|
||||
local f, err = io.open("/opt/bunkerweb/cache/blacklist/" .. kind .. ".list", "r")
|
||||
if f then
|
||||
for line in f:lines() do
|
||||
table.insert(blacklists[kind], line)
|
||||
i = i + 1
|
||||
end
|
||||
f:close()
|
||||
end
|
||||
end
|
||||
-- Load them into datastore
|
||||
local ok, err = datastore:set("plugin_blacklist_list", cjson.encode(blacklists))
|
||||
if not ok then
|
||||
return false, "can't store Blacklist list into datastore : " .. err
|
||||
end
|
||||
return true, "successfully loaded " .. tostring(i) .. " bad IP/network/rDNS/ASN/User-Agent/URI"
|
||||
end
|
||||
|
||||
function _M:access()
|
||||
-- Check if access is needed
|
||||
local access_needed, err = utils.get_variable("USE_BLACKLIST")
|
||||
if access_needed == nil then
|
||||
return false, err
|
||||
end
|
||||
if access_needed ~= "yes" then
|
||||
return true, "Blacklist not activated"
|
||||
end
|
||||
|
||||
-- Check the cache
|
||||
local cached_ip, err = self:is_in_cache("ip" .. ngx.var.remote_addr)
|
||||
if cached_ip and cached_ip ~= "ok" then
|
||||
return true, "IP is in blacklist cache (info = " .. cached_ip .. ")", true, ngx.HTTP_FORBIDDEN
|
||||
end
|
||||
local cached_uri, err = self:is_in_cache("uri" .. ngx.var.uri)
|
||||
if cached_uri and cached_uri ~= "ok" then
|
||||
return true, "URI is in blacklist cache (info = " .. cached_uri .. ")", true, ngx.HTTP_FORBIDDEN
|
||||
end
|
||||
local cached_ua = true
|
||||
if ngx.var.http_user_agent then
|
||||
cached_ua, err = self:is_in_cache("ua" .. ngx.var.http_user_agent)
|
||||
if cached_ua and cached_ua ~= "ok" then
|
||||
return true, "User-Agent is in blacklist cache (info = " .. cached_ua .. ")", true, ngx.HTTP_FORBIDDEN
|
||||
end
|
||||
end
|
||||
if cached_ip and cached_uri and cached_ua then
|
||||
return true, "full request is in blacklist cache (not blacklisted)", nil, nil
|
||||
end
|
||||
|
||||
-- Get list
|
||||
local data, err = datastore:get("plugin_blacklist_list")
|
||||
if not data then
|
||||
return false, "can't get Blacklist list : " .. err, false, nil
|
||||
end
|
||||
local ok, blacklists = pcall(cjson.decode, data)
|
||||
if not ok then
|
||||
return false, "error while decoding blacklists : " .. blacklists, false, nil
|
||||
end
|
||||
|
||||
-- Return value
|
||||
local ret, ret_err = true, "success"
|
||||
|
||||
-- Check if IP is in IP/net blacklist
|
||||
local ip_net, err = utils.get_variable("BLACKLIST_IP")
|
||||
if ip_net and ip_net ~= "" then
|
||||
for element in ip_net:gmatch("%S+") do
|
||||
table.insert(blacklists["IP"], element)
|
||||
end
|
||||
end
|
||||
if not cached_ip then
|
||||
local ipm, err = ipmatcher.new(blacklists["IP"])
|
||||
if not ipm then
|
||||
ret = false
|
||||
ret_err = "can't instantiate ipmatcher " .. err
|
||||
else
|
||||
if ipm:match(ngx.var.remote_addr) then
|
||||
self:add_to_cache("ip" .. ngx.var.remote_addr, "ip/net")
|
||||
return ret, "client IP " .. ngx.var.remote_addr .. " is in blacklist", true, ngx.HTTP_FORBIDDEN
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Check if rDNS is in blacklist
|
||||
local rdns_global, err = utils.get_variable("BLACKLIST_RDNS_GLOBAL")
|
||||
local check = true
|
||||
if not rdns_global then
|
||||
logger.log(ngx.ERR, "BLACKLIST", "Error while getting BLACKLIST_RDNS_GLOBAL variable : " .. err)
|
||||
elseif rdns_global == "yes" then
|
||||
check, err = utils.ip_is_global(ngx.var.remote_addr)
|
||||
if check == nil then
|
||||
logger.log(ngx.ERR, "BLACKLIST", "Error while getting checking if IP is global : " .. err)
|
||||
end
|
||||
end
|
||||
if not cached_ip and check then
|
||||
local rdns, err = utils.get_rdns(ngx.var.remote_addr)
|
||||
if not rdns then
|
||||
ret = false
|
||||
ret_err = "error while trying to get reverse dns : " .. err
|
||||
else
|
||||
local rdns_list, err = utils.get_variable("BLACKLIST_RDNS")
|
||||
if rdns_list and rdns_list ~= "" then
|
||||
for element in rdns_list:gmatch("%S+") do
|
||||
table.insert(blacklists["RDNS"], element)
|
||||
end
|
||||
end
|
||||
for i, suffix in ipairs(blacklists["RDNS"]) do
|
||||
if rdns:sub(-#suffix) == suffix then
|
||||
self:add_to_cache("ip" .. ngx.var.remote_addr, "rDNS " .. suffix)
|
||||
return ret, "client IP " .. ngx.var.remote_addr .. " is in blacklist (info = rDNS " .. suffix .. ")", true, ngx.HTTP_FORBIDDEN
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Check if ASN is in blacklist
|
||||
if not cached_ip then
|
||||
if utils.ip_is_global(ngx.var.remote_addr) then
|
||||
local asn, err = utils.get_asn(ngx.var.remote_addr)
|
||||
if not asn then
|
||||
ret = false
|
||||
ret_err = "error while trying to get asn number : " .. err
|
||||
else
|
||||
local asn_list, err = utils.get_variable("BLACKLIST_ASN")
|
||||
if asn_list and asn_list ~= "" then
|
||||
for element in asn_list:gmatch("%S+") do
|
||||
table.insert(blacklists["ASN"], element)
|
||||
end
|
||||
end
|
||||
for i, asn_bl in ipairs(blacklists["ASN"]) do
|
||||
if tostring(asn) == asn_bl then
|
||||
self:add_to_cache("ip" .. ngx.var.remote_addr, "ASN " .. tostring(asn))
|
||||
return ret, "client IP " .. ngx.var.remote_addr .. " is in blacklist (kind = ASN " .. tostring(asn) .. ")", true, ngx.HTTP_FORBIDDEN
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- IP is not blacklisted
|
||||
local ok, err = self:add_to_cache("ip" .. ngx.var.remote_addr, "ok")
|
||||
if not ok then
|
||||
ret = false
|
||||
ret_err = err
|
||||
end
|
||||
|
||||
-- Check if User-Agent is in blacklist
|
||||
if not cached_ua and ngx.var.http_user_agent then
|
||||
local ua_list, err = utils.get_variable("BLACKLIST_USER_AGENT")
|
||||
if ua_list and ua_list ~= "" then
|
||||
for element in ua_list:gmatch("%S+") do
|
||||
table.insert(blacklists["USER_AGENT"], element)
|
||||
end
|
||||
end
|
||||
for i, ua_bl in ipairs(blacklists["USER_AGENT"]) do
|
||||
if ngx.var.http_user_agent:match(ua_bl) then
|
||||
self:add_to_cache("ua" .. ngx.var.http_user_agent, "UA " .. ua_bl)
|
||||
return ret, "client User-Agent " .. ngx.var.http_user_agent .. " is in blacklist (matched " .. ua_bl .. ")", true, ngx.HTTP_FORBIDDEN
|
||||
end
|
||||
end
|
||||
-- UA is not blacklisted
|
||||
local ok, err = self:add_to_cache("ua" .. ngx.var.http_user_agent, "ok")
|
||||
if not ok then
|
||||
ret = false
|
||||
ret_err = err
|
||||
end
|
||||
end
|
||||
|
||||
-- Check if URI is in blacklist
|
||||
if not cached_uri then
|
||||
local uri_list, err = utils.get_variable("BLACKLIST_URI")
|
||||
if uri_list and uri_list ~= "" then
|
||||
for element in uri_list:gmatch("%S+") do
|
||||
table.insert(blacklists["URI"], element)
|
||||
end
|
||||
end
|
||||
for i, uri_bl in ipairs(blacklists["URI"]) do
|
||||
if ngx.var.uri:match(uri_bl) then
|
||||
self:add_to_cache("uri" .. ngx.var.uri, "URI " .. uri_bl)
|
||||
return ret, "client URI " .. ngx.var.uri .. " is in blacklist (matched " .. uri_bl .. ")", true, ngx.HTTP_FORBIDDEN
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- URI is not blacklisted
|
||||
local ok, err = self:add_to_cache("uri" .. ngx.var.uri, "ok")
|
||||
if not ok then
|
||||
ret = false
|
||||
ret_err = err
|
||||
end
|
||||
|
||||
return ret, "IP is not in list (error = " .. ret_err .. ")", false, nil
|
||||
end
|
||||
|
||||
function _M:is_in_cache(ele)
|
||||
local kind, err = datastore:get("plugin_blacklist_cache_" .. ngx.var.server_name .. ele)
|
||||
if not kind then
|
||||
if err ~= "not found" then
|
||||
logger.log(ngx.ERR, "BLACKLIST", "Error while accessing cache : " .. err)
|
||||
end
|
||||
return false, err
|
||||
end
|
||||
return kind, "success"
|
||||
end
|
||||
|
||||
function _M:add_to_cache(ele, kind)
|
||||
local ok, err = datastore:set("plugin_blacklist_cache_" .. ngx.var.server_name .. ele, kind, 3600)
|
||||
if not ok then
|
||||
logger.log(ngx.ERR, "BLACKLIST", "Error while adding element to cache : " .. err)
|
||||
return false, err
|
||||
end
|
||||
return true, "success"
|
||||
end
|
||||
|
||||
return _M
|
||||
152
core/blacklist/jobs/blacklist-download.py
Executable file
152
core/blacklist/jobs/blacklist-download.py
Executable file
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import sys, os, traceback
|
||||
|
||||
sys.path.append("/opt/bunkerweb/deps/python")
|
||||
sys.path.append("/opt/bunkerweb/utils")
|
||||
|
||||
import logger, jobs, requests, ipaddress
|
||||
|
||||
def check_line(kind, line) :
|
||||
if kind == "IP" :
|
||||
if "/" in line :
|
||||
try :
|
||||
ipaddress.ip_network(line)
|
||||
return True, line
|
||||
except :
|
||||
pass
|
||||
else :
|
||||
try :
|
||||
ipaddress.ip_address(line)
|
||||
return True, line
|
||||
except :
|
||||
pass
|
||||
return False, ""
|
||||
elif kind == "RDNS" :
|
||||
if re.match(r"^(\.?[A-Za-z0-9\-]+)*\.[A-Za-z]{2,}$", line) :
|
||||
return True, line.lower()
|
||||
return False, ""
|
||||
elif kind == "ASN" :
|
||||
real_line = line.replace("AS", "")
|
||||
if re.match(r"^\d+$", real_line) :
|
||||
return True, real_line
|
||||
elif kind == "USER_AGENT" :
|
||||
return True, line.replace("\\ ", " ").replace("\\.", "%.").replace("\\\\", "\\").replace("-", "%-")
|
||||
elif kind == "URI" :
|
||||
if re.match(r"^/", line) :
|
||||
return True, line
|
||||
return False, ""
|
||||
|
||||
status = 0
|
||||
|
||||
try :
|
||||
|
||||
# Check if at least a server has Blacklist activated
|
||||
blacklist_activated = False
|
||||
# Multisite case
|
||||
if os.getenv("MULTISITE") == "yes" :
|
||||
for first_server in os.getenv("SERVER_NAME").split(" ") :
|
||||
if os.getenv(first_server + "_USE_BLACKLIST", os.getenv("USE_BLACKLIST")) == "yes" :
|
||||
blacklist_activated = True
|
||||
break
|
||||
# Singlesite case
|
||||
elif os.getenv("USE_BLACKLIST") == "yes" :
|
||||
blacklist_activated = True
|
||||
if not blacklist_activated :
|
||||
logger.log("BLACKLIST", "ℹ️", "Blacklist is not activated, skipping downloads...")
|
||||
os._exit(0)
|
||||
|
||||
# Create directories if they don't exist
|
||||
os.makedirs("/opt/bunkerweb/cache/blacklist", exist_ok=True)
|
||||
os.makedirs("/opt/bunkerweb/tmp/blacklist", exist_ok=True)
|
||||
|
||||
# Our urls data
|
||||
urls = {
|
||||
"IP": [],
|
||||
"RDNS": [],
|
||||
"ASN" : [],
|
||||
"USER_AGENT": [],
|
||||
"URI": []
|
||||
}
|
||||
|
||||
# Don't go further if the cache is fresh
|
||||
kinds_fresh = {
|
||||
"IP": True,
|
||||
"RDNS": True,
|
||||
"ASN" : True,
|
||||
"USER_AGENT": True,
|
||||
"URI": True
|
||||
}
|
||||
all_fresh = True
|
||||
for kind in kinds_fresh :
|
||||
if not jobs.is_cached_file("/opt/bunkerweb/cache/blacklist/" + kind + ".list", "hour") :
|
||||
kinds_fresh[kind] = False
|
||||
all_fresh = False
|
||||
logger.log("BLACKLIST", "ℹ️", "Blacklist for " + kind + " is not cached, processing downloads..")
|
||||
else :
|
||||
logger.log("BLACKLIST", "ℹ️", "Blacklist for " + kind + " is already in cache, skipping downloads...")
|
||||
if all_fresh :
|
||||
os._exit(0)
|
||||
|
||||
# Get URLs
|
||||
urls = {
|
||||
"IP": [],
|
||||
"RDNS": [],
|
||||
"ASN" : [],
|
||||
"USER_AGENT": [],
|
||||
"URI": []
|
||||
}
|
||||
for kind in urls :
|
||||
for url in os.getenv("BLACKLIST_" + kind + "_URLS", "").split(" ") :
|
||||
if url != "" and url not in urls[kind] :
|
||||
urls[kind].append(url)
|
||||
|
||||
# Loop on kinds
|
||||
for kind, urls_list in urls.items() :
|
||||
if kinds_fresh[kind] :
|
||||
continue
|
||||
# Write combined data of the kind to a single temp file
|
||||
for url in urls_list :
|
||||
try :
|
||||
logger.log("BLACKLIST", "ℹ️", "Downloading blacklist data from " + url + " ...")
|
||||
resp = requests.get(url, stream=True)
|
||||
if resp.status_code != 200 :
|
||||
continue
|
||||
i = 0
|
||||
with open("/opt/bunkerweb/tmp/blacklist/" + kind + ".list", "w") as f :
|
||||
for line in resp.iter_lines(decode_unicode=True) :
|
||||
line = line.strip()
|
||||
if kind != "USER_AGENT" :
|
||||
line = line.strip().split(" ")[0]
|
||||
if line == "" or line.startswith("#") or line.startswith(";") :
|
||||
continue
|
||||
ok, data = check_line(kind, line)
|
||||
if ok :
|
||||
f.write(data + "\n")
|
||||
i += 1
|
||||
logger.log("BLACKLIST", "ℹ️", "Downloaded " + str(i) + " bad " + kind)
|
||||
# Check if file has changed
|
||||
file_hash = jobs.file_hash("/opt/bunkerweb/tmp/blacklist/" + kind + ".list")
|
||||
cache_hash = jobs.cache_hash("/opt/bunkerweb/cache/blacklist/" + kind + ".list")
|
||||
if file_hash == cache_hash :
|
||||
logger.log("BLACKLIST", "ℹ️", "New file " + kind + ".list is identical to cache file, reload is not needed")
|
||||
else :
|
||||
logger.log("BLACKLIST", "ℹ️", "New file " + kind + ".list is different than cache file, reload is needed")
|
||||
# Put file in cache
|
||||
cached, err = jobs.cache_file("/opt/bunkerweb/tmp/blacklist/" + kind + ".list", "/opt/bunkerweb/cache/blacklist/" + kind + ".list", file_hash)
|
||||
if not cached :
|
||||
logger.log("BLACKLIST", "❌", "Error while caching blacklist : " + err)
|
||||
status = 2
|
||||
if status != 2 :
|
||||
status = 1
|
||||
except :
|
||||
status = 2
|
||||
logger.log("BLACKLIST", "❌", "Exception while getting blacklist from " + url + " :")
|
||||
print(traceback.format_exc())
|
||||
|
||||
except :
|
||||
status = 2
|
||||
logger.log("BLACKLIST", "❌", "Exception while running blacklist-download.py :")
|
||||
print(traceback.format_exc())
|
||||
|
||||
sys.exit(status)
|
||||
125
core/blacklist/plugin.json
Normal file
125
core/blacklist/plugin.json
Normal file
@@ -0,0 +1,125 @@
|
||||
{
|
||||
"id": "blacklist",
|
||||
"order": 2,
|
||||
"name": "Blacklist",
|
||||
"description": "Deny access based on internal and external IP/network/rDNS/ASN blacklists.",
|
||||
"version": "0.1",
|
||||
"settings": {
|
||||
"USE_BLACKLIST": {
|
||||
"context": "multisite",
|
||||
"default": "yes",
|
||||
"help": "Activate blacklist feature.",
|
||||
"id": "use-blacklist",
|
||||
"label": "Activate blacklisting",
|
||||
"regex": "^(yes|no)$",
|
||||
"type": "check"
|
||||
},
|
||||
"BLACKLIST_IP_URLS": {
|
||||
"context": "global",
|
||||
"default": "https://www.dan.me.uk/torlist/?exit",
|
||||
"help": "List of URLs, separated with spaces, containing bad IP/network to block.",
|
||||
"id": "blacklist-ip-urls",
|
||||
"label": "Blacklist IP/network URLs",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"BLACKLIST_IP": {
|
||||
"context": "multisite",
|
||||
"default": "",
|
||||
"help": "List of IP/network, separated with spaces, to block.",
|
||||
"id": "blacklist-ip",
|
||||
"label": "Blacklist IP/network",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"BLACKLIST_RDNS": {
|
||||
"context": "multisite",
|
||||
"default": ".shodan.io .censys.io",
|
||||
"help": "List of reverse DNS suffixes, separated with spaces, to block.",
|
||||
"id": "blacklist-rdns",
|
||||
"label": "Blacklist reverse DNS",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"BLACKLIST_RDNS_URLS": {
|
||||
"context": "global",
|
||||
"default": "",
|
||||
"help": "List of URLs, separated with spaces, containing reverse DNS suffixes to block.",
|
||||
"id": "blacklist-rdns-urls",
|
||||
"label": "Blacklist reverse DNS URLs",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"BLACKLIST_RDNS_GLOBAL": {
|
||||
"context": "multisite",
|
||||
"default": "yes",
|
||||
"help": "Only perform RDNS blacklist checks on global IP addresses.",
|
||||
"id": "blacklist-rdns-global",
|
||||
"label": "Blacklist reverse DNS global IPs",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"BLACKLIST_ASN": {
|
||||
"context": "multisite",
|
||||
"default": "",
|
||||
"help": "List of ASN numbers, separated with spaces, to block.",
|
||||
"id": "blacklist-asn",
|
||||
"label": "Blacklist ASN",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"BLACKLIST_ASN_URLS": {
|
||||
"context": "global",
|
||||
"default": "",
|
||||
"help": "List of URLs, separated with spaces, containing ASN to block.",
|
||||
"id": "blacklist-rdns-urls",
|
||||
"label": "Blacklist ASN URLs",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"BLACKLIST_USER_AGENT": {
|
||||
"context": "multisite",
|
||||
"default": "",
|
||||
"help": "List of User-Agent, separated with spaces, to block.",
|
||||
"id": "blacklist-user-agent",
|
||||
"label": "Blacklist User-Agent",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"BLACKLIST_USER_AGENT_URLS": {
|
||||
"context": "global",
|
||||
"default": "https://raw.githubusercontent.com/mitchellkrogza/nginx-ultimate-bad-bot-blocker/master/_generator_lists/bad-user-agents.list",
|
||||
"help": "List of URLs, separated with spaces, containing bad User-Agent to block.",
|
||||
"id": "blacklist-user-agent-urls",
|
||||
"label": "Blacklist User-Agent URLs",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"BLACKLIST_URI": {
|
||||
"context": "multisite",
|
||||
"default": "",
|
||||
"help": "List of URI, separated with spaces, to block.",
|
||||
"id": "blacklist-uri",
|
||||
"label": "Blacklist URI",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"BLACKLIST_URI_URLS": {
|
||||
"context": "global",
|
||||
"default": "",
|
||||
"help": "List of URLs, separated with spaces, containing bad URI to block.",
|
||||
"id": "blacklist-uri-urls",
|
||||
"label": "Blacklist URI URLs",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
}
|
||||
},
|
||||
"jobs": [
|
||||
{
|
||||
"name": "blacklist-download",
|
||||
"file": "blacklist-download.py",
|
||||
"every": "hour",
|
||||
"reload": true
|
||||
}
|
||||
]
|
||||
}
|
||||
6
core/brotli/confs/server-http/brotli.conf
Normal file
6
core/brotli/confs/server-http/brotli.conf
Normal file
@@ -0,0 +1,6 @@
|
||||
{% if USE_BROTLI == "yes" +%}
|
||||
brotli on;
|
||||
brotli_types {{ BROTLI_TYPES }};
|
||||
brotli_comp_level {{ BROTLI_COMP_LEVEL }};
|
||||
brotli_min_length {{ BROTLI_MIN_LENGTH }};
|
||||
{% endif %}
|
||||
58
core/brotli/plugin.json
Normal file
58
core/brotli/plugin.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"id": "brotli",
|
||||
"order": 999,
|
||||
"name": "Brotli",
|
||||
"description": "Compress HTTP requests with the brotli algorithm.",
|
||||
"version": "0.1",
|
||||
"settings": {
|
||||
"USE_BROTLI": {
|
||||
"context": "multisite",
|
||||
"default": "no",
|
||||
"help": "Use brotli",
|
||||
"id": "use-brotli",
|
||||
"label": "Use brotli",
|
||||
"regex": "^(yes|no)$",
|
||||
"type": "check"
|
||||
},
|
||||
"BROTLI_TYPES": {
|
||||
"context": "multisite",
|
||||
"default": "application/atom+xml application/javascript application/json application/rss+xml application/vnd.ms-fontobject application/x-font-opentype application/x-font-truetype application/x-font-ttf application/x-javascript application/xhtml+xml application/xml font/eot font/opentype font/otf font/truetype image/svg+xml image/vnd.microsoft.icon image/x-icon image/x-win-bitmap text/css text/javascript text/plain text/xml",
|
||||
"help": "List of MIME types that will be compressed with brotli.",
|
||||
"id": "brotli-types",
|
||||
"label": "MIME types",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"BROTLI_MIN_LENGTH": {
|
||||
"context": "multisite",
|
||||
"default": "1000",
|
||||
"help": "Minimum length for brotli compression.",
|
||||
"id": "brotli-min-length",
|
||||
"label": "Minimum length",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"BROTLI_COMP_LEVEL": {
|
||||
"context": "multisite",
|
||||
"default": "6",
|
||||
"help": "The compression level of the brotli algorithm.",
|
||||
"id": "brotli-comp-level",
|
||||
"label": "Compression level",
|
||||
"regex": "^([1-9]|10|11)$",
|
||||
"type": "select",
|
||||
"select": [
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
"10",
|
||||
"11"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
222
core/bunkernet/bunkernet.lua
Normal file
222
core/bunkernet/bunkernet.lua
Normal file
@@ -0,0 +1,222 @@
|
||||
local _M = {}
|
||||
_M.__index = _M
|
||||
|
||||
local utils = require "utils"
|
||||
local datastore = require "datastore"
|
||||
local logger = require "logger"
|
||||
local cjson = require "cjson"
|
||||
local http = require "resty.http"
|
||||
|
||||
function _M.new()
|
||||
local self = setmetatable({}, _M)
|
||||
local server, err = datastore:get("variable_BUNKERNET_SERVER")
|
||||
if not server then
|
||||
return nil, "can't get BUNKERNET_SERVER from datastore : " .. err
|
||||
end
|
||||
self.server = server
|
||||
local id, err = datastore:get("plugin_bunkernet_id")
|
||||
if not id then
|
||||
self.id = nil
|
||||
else
|
||||
self.id = id
|
||||
end
|
||||
return self, nil
|
||||
end
|
||||
|
||||
function _M:init()
|
||||
local init_needed, err = utils.has_variable("USE_BUNKERNET", "yes")
|
||||
if init_needed == nil then
|
||||
return false, err
|
||||
end
|
||||
if not init_needed then
|
||||
return true, "no service uses BunkerNet, skipping init"
|
||||
end
|
||||
-- Check if instance ID is present
|
||||
local f, err = io.open("/opt/bunkerweb/cache/bunkernet/instance.id", "r")
|
||||
if not f then
|
||||
return false, "can't read instance id : " .. err
|
||||
end
|
||||
-- Retrieve instance ID
|
||||
id = f:read("*all"):gsub("[\r\n]", "")
|
||||
f:close()
|
||||
self.id = id
|
||||
-- TODO : regex check just in case
|
||||
-- Send a ping with the ID
|
||||
--local ok, err, status, response = self:ping()
|
||||
-- BunkerNet server is down or instance can't access it
|
||||
--if not ok then
|
||||
--return false, "can't send request to BunkerNet service : " .. err
|
||||
-- Local instance ID is unknown to the server, let's delete it
|
||||
--elseif status == 401 then
|
||||
--local ok, message = os.remove("/opt/bunkerweb/cache/bunkernet/instance.id")
|
||||
--if not ok then
|
||||
--return false, "can't remove instance ID " .. message
|
||||
--end
|
||||
--return false, "instance ID is not valid"
|
||||
--elseif status == 429 then
|
||||
--return false, "sent too many requests to the BunkerNet service"
|
||||
--elseif status ~= 200 then
|
||||
--return false, "unknown error from BunkerNet service (HTTP status = " .. tostring(status) .. ")"
|
||||
--end
|
||||
-- Store ID in datastore
|
||||
local ok, err = datastore:set("plugin_bunkernet_id", id)
|
||||
if not ok then
|
||||
return false, "can't save instance ID to the datastore : " .. err
|
||||
end
|
||||
-- Load databases
|
||||
local ret = true
|
||||
local i = 0
|
||||
local db = {
|
||||
ip = {}
|
||||
}
|
||||
f, err = io.open("/opt/bunkerweb/cache/bunkernet/ip.list", "r")
|
||||
if not f then
|
||||
ret = false
|
||||
else
|
||||
for line in f:lines() do
|
||||
if utils.is_ipv4(line) and utils.ip_is_global(line) then
|
||||
table.insert(db.ip, line)
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
if not ret then
|
||||
return false, "error while reading database : " .. err
|
||||
end
|
||||
f:close()
|
||||
local ok, err = datastore:set("plugin_bunkernet_db", cjson.encode(db))
|
||||
if not ok then
|
||||
return false, "can't store BunkerNet database into datastore : " .. err
|
||||
end
|
||||
return true, "successfully connected to the BunkerNet service " .. self.server .. " with machine ID " .. id .. " and " .. tostring(i) .. " bad IPs in database"
|
||||
end
|
||||
|
||||
function _M:request(method, url, data)
|
||||
local httpc, err = http.new()
|
||||
if not httpc then
|
||||
return false, "can't instantiate http object : " .. err, nil, nil
|
||||
end
|
||||
local all_data = {
|
||||
id = self.id,
|
||||
integration = utils.get_integration(),
|
||||
version = utils.get_version()
|
||||
}
|
||||
for k, v in pairs(data) do
|
||||
all_data[k] = v
|
||||
end
|
||||
local res, err = httpc:request_uri(self.server .. url, {
|
||||
method = method,
|
||||
body = cjson.encode(all_data),
|
||||
headers = {
|
||||
["Content-Type"] = "application/json",
|
||||
["User-Agent"] = "BunkerWeb/" .. utils.get_version()
|
||||
}
|
||||
})
|
||||
httpc:close()
|
||||
if not res then
|
||||
return false, "error while sending request : " .. err, nil, nil
|
||||
end
|
||||
if res.status ~= 200 then
|
||||
return false, "status code != 200", res.status, nil
|
||||
end
|
||||
local ok, ret = pcall(cjson.decode, res.body)
|
||||
if not ok then
|
||||
return false, "error while decoding json : " .. ret, nil, nil
|
||||
end
|
||||
return true, "success", res.status, ret
|
||||
end
|
||||
|
||||
function _M:ping()
|
||||
return self:request("GET", "/ping", {})
|
||||
end
|
||||
|
||||
function _M:report(ip, reason, method, url, headers)
|
||||
local data = {
|
||||
ip = ip,
|
||||
reason = reason,
|
||||
method = method,
|
||||
url = url,
|
||||
headers = headers
|
||||
}
|
||||
return self:request("POST", "/report", data)
|
||||
end
|
||||
|
||||
function _M:log()
|
||||
-- Check if BunkerNet is activated
|
||||
local use_bunkernet = utils.get_variable("USE_BUNKERNET")
|
||||
if use_bunkernet ~= "yes" then
|
||||
return true, "bunkernet not activated"
|
||||
end
|
||||
-- Check if BunkerNet ID is generated
|
||||
if not self.id then
|
||||
return true, "bunkernet ID is not generated"
|
||||
end
|
||||
-- Check if IP has been blocked
|
||||
local reason = utils.get_reason()
|
||||
if not reason then
|
||||
return true, "ip is not blocked"
|
||||
end
|
||||
if reason == "bunkernet" then
|
||||
return true, "skipping report because the reason is bunkernet"
|
||||
end
|
||||
-- Check if IP is global
|
||||
local is_global, err = utils.ip_is_global(ngx.var.remote_addr)
|
||||
if is_global == nil then
|
||||
return false, "error while checking if IP is global " .. err
|
||||
end
|
||||
if not is_global then
|
||||
return true, "IP is not global"
|
||||
end
|
||||
-- Only report if it hasn't been reported for the same reason recently
|
||||
--local reported = datastore:get("plugin_bunkernet_cache_" .. ngx.var.remote_addr .. reason)
|
||||
--if reported then
|
||||
--return true, "ip already reported recently"
|
||||
--end
|
||||
local function report_callback(premature, obj, ip, reason, method, url, headers)
|
||||
local ok, err, status, data = obj:report(ip, reason, method, url, headers)
|
||||
if status == 429 then
|
||||
logger.log(ngx.WARN, "BUNKERNET", "BunkerNet API is rate limiting us")
|
||||
elseif not ok then
|
||||
logger.log(ngx.ERR, "BUNKERNET", "Can't report IP : " .. err)
|
||||
else
|
||||
logger.log(ngx.NOTICE, "BUNKERNET", "Successfully reported IP " .. ip .. " (reason : " .. reason .. ")")
|
||||
--local ok, err = datastore:set("plugin_bunkernet_cache_" .. ip .. reason, true, 3600)
|
||||
--if not ok then
|
||||
--logger.log(ngx.ERR, "BUNKERNET", "Can't store cached report : " .. err)
|
||||
--end
|
||||
end
|
||||
end
|
||||
local hdr, err = ngx.timer.at(0, report_callback, self, ngx.var.remote_addr, reason, ngx.var.request_method, ngx.var.request_uri, ngx.req.get_headers())
|
||||
if not hdr then
|
||||
return false, "can't create report timer : " .. err
|
||||
end
|
||||
return true, "created report timer"
|
||||
end
|
||||
|
||||
function _M:access()
|
||||
local use_bunkernet = utils.get_variable("USE_BUNKERNET")
|
||||
if use_bunkernet ~= "yes" then
|
||||
return true, "bunkernet not activated", false, nil
|
||||
end
|
||||
-- Check if BunkerNet ID is generated
|
||||
if not self.id then
|
||||
return true, "bunkernet ID is not generated"
|
||||
end
|
||||
local data, err = datastore:get("plugin_bunkernet_db")
|
||||
if not data then
|
||||
return false, "can't get bunkernet db : " .. err, false, nil
|
||||
end
|
||||
local db = cjson.decode(data)
|
||||
for index, value in ipairs(db.ip) do
|
||||
if value == ngx.var.remote_addr then
|
||||
return true, "ip is in database", true, ngx.exit(ngx.HTTP_FORBIDDEN)
|
||||
end
|
||||
end
|
||||
return true, "ip is not in database", false, nil
|
||||
end
|
||||
|
||||
function _M:api()
|
||||
return false, nil, nil
|
||||
end
|
||||
|
||||
return _M
|
||||
51
core/bunkernet/confs/default-server-http/bunkernet.conf
Normal file
51
core/bunkernet/confs/default-server-http/bunkernet.conf
Normal file
@@ -0,0 +1,51 @@
|
||||
log_by_lua_block {
|
||||
local bunkernet = require "bunkernet.bunkernet"
|
||||
local utils = require "utils"
|
||||
local datastore = require "datastore"
|
||||
local logger = require "logger"
|
||||
local disable_default_server = utils.get_variable("DISABLE_DEFAULT_SERVER", false)
|
||||
local use_bunkernet = utils.has_variable("USE_BUNKERNET", "yes")
|
||||
|
||||
if disable_default_server == "yes" and use_bunkernet then
|
||||
-- Instantiate bunkernet
|
||||
local bnet, err = bunkernet.new()
|
||||
if not bnet then
|
||||
ngx.log(ngx.ERR, "BUNKERNET", "can't instantiate bunkernet " .. err)
|
||||
return
|
||||
end
|
||||
-- Check if BunkerNet ID is generated
|
||||
if not bnet.id then
|
||||
return
|
||||
end
|
||||
-- Check if IP has been blocked
|
||||
if ngx.status ~= ngx.HTTP_CLOSE then
|
||||
return
|
||||
end
|
||||
-- Only report if it hasn't been reported for the same reason recently
|
||||
local reported = datastore:get("plugin_bunkernet_cache_" .. ngx.var.remote_addr .. "default")
|
||||
if reported then
|
||||
return
|
||||
end
|
||||
-- report callback called in a light thread
|
||||
local function report_callback(premature, obj, ip, reason, method, url, headers)
|
||||
local ok, err, status, data = obj:report(ip, reason, method, url, headers)
|
||||
if not ok then
|
||||
logger.log(ngx.ERR, "BUNKERNET", "Can't report IP : " .. err)
|
||||
elseif status ~= 200 then
|
||||
logger.log(ngx.ERR, "BUNKERNET", "Error from remote server : " .. tostring(status))
|
||||
else
|
||||
logger.log(ngx.NOTICE, "BUNKERNET", "Successfully reported IP " .. ip .. " (reason : " .. reason .. ")")
|
||||
local ok, err = datastore:set("plugin_bunkernet_cache_" .. ip .. reason, true, 3600)
|
||||
if not ok then
|
||||
logger.log(ngx.ERR, "BUNKERNET", "Can't store cached report : " .. err)
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Set a timer at the end of log()
|
||||
local hdr, err = ngx.timer.at(0, report_callback, bnet, ngx.var.remote_addr, "default", ngx.var.request_method, ngx.var.request_uri, ngx.req.get_headers())
|
||||
if not hdr then
|
||||
logger.log(ngx.ERR, "BUNKERNET", "can't create report timer : " .. err)
|
||||
end
|
||||
return
|
||||
end
|
||||
}
|
||||
85
core/bunkernet/jobs/bunkernet-data.py
Executable file
85
core/bunkernet/jobs/bunkernet-data.py
Executable file
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import sys, os, traceback
|
||||
|
||||
sys.path.append("/opt/bunkerweb/deps/python")
|
||||
sys.path.append("/opt/bunkerweb/utils")
|
||||
sys.path.append("/opt/bunkerweb/core/bunkernet/jobs")
|
||||
|
||||
import logger, jobs
|
||||
from bunkernet import data
|
||||
|
||||
status = 0
|
||||
|
||||
try :
|
||||
|
||||
# Check if at least a server has BunkerNet activated
|
||||
bunkernet_activated = False
|
||||
# Multisite case
|
||||
if os.getenv("MULTISITE") == "yes" :
|
||||
for first_server in os.getenv("SERVER_NAME").split(" ") :
|
||||
if os.getenv(first_server + "_USE_BUNKERNET", os.getenv("USE_BUNKERNET")) == "yes" :
|
||||
bunkernet_activated = True
|
||||
break
|
||||
# Singlesite case
|
||||
elif os.getenv("USE_BUNKERNET") == "yes" :
|
||||
bunkernet_activated = True
|
||||
if not bunkernet_activated :
|
||||
logger.log("BUNKERNET", "ℹ️", "BunkerNet is not activated, skipping download...")
|
||||
os._exit(0)
|
||||
|
||||
# Create directory if it doesn't exist
|
||||
os.makedirs("/opt/bunkerweb/cache/bunkernet", exist_ok=True)
|
||||
|
||||
# Check if ID is present
|
||||
if not os.path.isfile("/opt/bunkerweb/cache/bunkernet/instance.id") :
|
||||
logger.log("BUNKERNET", "❌", "Not downloading BunkerNet data because instance is not registered")
|
||||
os._exit(2)
|
||||
|
||||
# Don't go further if the cache is fresh
|
||||
if jobs.is_cached_file("/opt/bunkerweb/cache/bunkernet/ip.list", "day") :
|
||||
logger.log("BUNKERNET", "ℹ️", "BunkerNet list is already in cache, skipping download...")
|
||||
os._exit(0)
|
||||
|
||||
# Download data
|
||||
logger.log("BUNKERNET", "ℹ️", "Downloading BunkerNet data ...")
|
||||
ok, status, data = data()
|
||||
if not ok :
|
||||
logger.log("BUNKERNET", "❌", "Error while sending data request to BunkerNet API : " + data)
|
||||
os._exit(2)
|
||||
elif status == 429 :
|
||||
logger.log("BUNKERNET", "⚠️", "BunkerNet API is rate limiting us, trying again later...")
|
||||
os._exit(0)
|
||||
elif data["result"] != "ok" :
|
||||
logger.log("BUNKERNET", "❌", "Received error from BunkerNet API while sending db request : " + data["data"] + ", removing instance ID")
|
||||
os._exit(2)
|
||||
logger.log("BUNKERNET", "ℹ️", "Successfully downloaded data from BunkerNet API")
|
||||
|
||||
# Writing data to file
|
||||
logger.log("BUNKERNET", "ℹ️", "Saving BunkerNet data ...")
|
||||
with open("/opt/bunkerweb/tmp/bunkernet-ip.list", "w") as f :
|
||||
for ip in data["data"] :
|
||||
f.write(ip + "\n")
|
||||
|
||||
# Check if file has changed
|
||||
file_hash = jobs.file_hash("/opt/bunkerweb/tmp/bunkernet-ip.list")
|
||||
cache_hash = jobs.cache_hash("/opt/bunkerweb/cache/bunkernet/ip.list")
|
||||
if file_hash == cache_hash :
|
||||
logger.log("BUNKERNET", "ℹ️", "New file is identical to cache file, reload is not needed")
|
||||
os._exit(0)
|
||||
|
||||
# Put file in cache
|
||||
cached, err = jobs.cache_file("/opt/bunkerweb/tmp/bunkernet-ip.list", "/opt/bunkerweb/cache/bunkernet/ip.list", file_hash)
|
||||
if not cached :
|
||||
logger.log("BUNKERNET", "❌", "Error while caching BunkerNet data : " + err)
|
||||
os._exit(2)
|
||||
logger.log("BUNKERNET", "ℹ️", "Successfully saved BunkerNet data")
|
||||
|
||||
status = 1
|
||||
|
||||
except :
|
||||
status = 2
|
||||
logger.log("BUNKERNET", "❌", "Exception while running bunkernet-data.py :")
|
||||
print(traceback.format_exc())
|
||||
|
||||
sys.exit(status)
|
||||
81
core/bunkernet/jobs/bunkernet-register.py
Executable file
81
core/bunkernet/jobs/bunkernet-register.py
Executable file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import sys, os, traceback
|
||||
|
||||
sys.path.append("/opt/bunkerweb/deps/python")
|
||||
sys.path.append("/opt/bunkerweb/utils")
|
||||
sys.path.append("/opt/bunkerweb/core/bunkernet/jobs")
|
||||
|
||||
import logger
|
||||
from bunkernet import register, ping, get_id
|
||||
|
||||
status = 0
|
||||
|
||||
try :
|
||||
|
||||
# Check if at least a server has BunkerNet activated
|
||||
bunkernet_activated = False
|
||||
# Multisite case
|
||||
if os.getenv("MULTISITE") == "yes" :
|
||||
for first_server in os.getenv("SERVER_NAME").split(" ") :
|
||||
if os.getenv(first_server + "_USE_BUNKERNET", os.getenv("USE_BUNKERNET")) == "yes" :
|
||||
bunkernet_activated = True
|
||||
break
|
||||
# Singlesite case
|
||||
elif os.getenv("USE_BUNKERNET") == "yes" :
|
||||
bunkernet_activated = True
|
||||
if not bunkernet_activated :
|
||||
logger.log("BUNKERNET", "ℹ️", "BunkerNet is not activated, skipping registration...")
|
||||
os._exit(0)
|
||||
|
||||
# Create directory if it doesn't exist
|
||||
os.makedirs("/opt/bunkerweb/cache/bunkernet", exist_ok=True)
|
||||
|
||||
# Ask an ID if needed
|
||||
if not os.path.isfile("/opt/bunkerweb/cache/bunkernet/instance.id") :
|
||||
logger.log("BUNKERNET", "ℹ️", "Registering instance on BunkerNet API ...")
|
||||
ok, status, data = register()
|
||||
if not ok :
|
||||
logger.log("BUNKERNET", "❌", "Error while sending register request to BunkerNet API : " + data)
|
||||
os._exit(1)
|
||||
elif status == 429 :
|
||||
logger.log("BUNKERNET", "⚠️", "BunkerNet API is rate limiting us, trying again later...")
|
||||
os._exit(0)
|
||||
elif status != 200 :
|
||||
logger.log("BUNKERNET", "❌", "Error " + str(status) + " from BunkerNet API : " + data["data"])
|
||||
os._exit(1)
|
||||
elif data["result"] != "ok" :
|
||||
logger.log("BUNKERNET", "❌", "Received error from BunkerNet API while sending register request : " + data["data"])
|
||||
os._exit(1)
|
||||
with open("/opt/bunkerweb/cache/bunkernet/instance.id", "w") as f :
|
||||
f.write(data["data"])
|
||||
logger.log("BUNKERNET", "ℹ️", "Successfully registered on BunkerNet API with instance id " + get_id())
|
||||
else :
|
||||
logger.log("BUNKERNET", "ℹ️", "Already registered on BunkerNet API with instance id " + get_id())
|
||||
|
||||
# Ping
|
||||
logger.log("BUNKERNET", "ℹ️", "Checking connectivity with BunkerNet API ...")
|
||||
ok, status, data = ping()
|
||||
if not ok :
|
||||
logger.log("BUNKERNET", "❌", "Error while sending ping request to BunkerNet API : " + data)
|
||||
os._exit(2)
|
||||
elif status == 429 :
|
||||
logger.log("BUNKERNET", "⚠️", "BunkerNet API is rate limiting us, trying again later...")
|
||||
os._exit(0)
|
||||
elif status == 401 :
|
||||
logger.log("BUNKERNET", "⚠️", "Instance ID is not registered, removing it and retrying a register later...")
|
||||
os.remove("/opt/bunkerweb/cache/bunkernet/instance.id")
|
||||
os._exit(1)
|
||||
elif data["result"] != "ok" :
|
||||
logger.log("BUNKERNET", "❌", "Received error from BunkerNet API while sending ping request : " + data["data"] + ", removing instance ID")
|
||||
os._exit(1)
|
||||
logger.log("BUNKERNET", "ℹ️", "Successfully checked connectivity with BunkerNet API")
|
||||
|
||||
status = 1
|
||||
|
||||
except :
|
||||
status = 2
|
||||
logger.log("BUNKERNET", "❌", "Exception while running bunkernet-register.py :")
|
||||
print(traceback.format_exc())
|
||||
|
||||
sys.exit(status)
|
||||
55
core/bunkernet/jobs/bunkernet.py
Normal file
55
core/bunkernet/jobs/bunkernet.py
Normal file
@@ -0,0 +1,55 @@
|
||||
import requests, traceback
|
||||
from os import getenv
|
||||
|
||||
def request(method, url, _id=None) :
|
||||
data = {
|
||||
"integration": get_integration(),
|
||||
"version": get_version()
|
||||
}
|
||||
headers = {
|
||||
"User-Agent": "BunkerWeb/" + get_version()
|
||||
}
|
||||
if _id is not None :
|
||||
data["id"] = _id
|
||||
try :
|
||||
resp = requests.request(method, getenv("BUNKERNET_SERVER") + url, json=data, headers=headers, timeout=5)
|
||||
status = resp.status_code
|
||||
if status == 429 :
|
||||
return True, 429, "rate limited"
|
||||
raw_data = resp.json()
|
||||
assert "result" in raw_data
|
||||
assert "data" in raw_data
|
||||
except Exception as e :
|
||||
return False, None, traceback.format_exc()
|
||||
return True, status, raw_data
|
||||
|
||||
def register() :
|
||||
return request("POST", "/register")
|
||||
|
||||
def ping() :
|
||||
return request("GET", "/ping", _id=get_id())
|
||||
|
||||
def data() :
|
||||
return request("GET", "/db", _id=get_id())
|
||||
|
||||
def get_id() :
|
||||
with open("/opt/bunkerweb/cache/bunkernet/instance.id", "r") as f :
|
||||
return f.read().strip()
|
||||
|
||||
def get_version() :
|
||||
with open("/opt/bunkerweb/VERSION", "r") as f :
|
||||
return f.read().strip()
|
||||
|
||||
def get_integration() :
|
||||
try :
|
||||
if getenv("KUBERNETES_MODE") == "yes" :
|
||||
return "kubernetes"
|
||||
if getenv("SWARM_MODE") == "yes" :
|
||||
return "swarm"
|
||||
with open("/etc/os-release", "r") as f :
|
||||
if f.read().contains("Alpine") :
|
||||
return "docker"
|
||||
return "linux"
|
||||
except :
|
||||
return "unknown"
|
||||
|
||||
41
core/bunkernet/plugin.json
Normal file
41
core/bunkernet/plugin.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"id": "bunkernet",
|
||||
"order": 2,
|
||||
"name": "BunkerNet",
|
||||
"description": "Share threat data with other BunkerWeb instances via BunkerNet.",
|
||||
"version": "0.1",
|
||||
"settings": {
|
||||
"USE_BUNKERNET": {
|
||||
"context": "multisite",
|
||||
"default": "yes",
|
||||
"help": "Activate BunkerNet feature.",
|
||||
"id": "use-bunkernet",
|
||||
"label": "Activate BunkerNet",
|
||||
"regex": "^(yes|no)$",
|
||||
"type": "check"
|
||||
},
|
||||
"BUNKERNET_SERVER": {
|
||||
"context": "global",
|
||||
"default": "https://api.bunkerweb.io",
|
||||
"help": "Address of the BunkerNet API.",
|
||||
"id": "bunkernet-server",
|
||||
"label": "BunkerNet server",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
}
|
||||
},
|
||||
"jobs": [
|
||||
{
|
||||
"name": "bunkernet-register",
|
||||
"file": "bunkernet-register.py",
|
||||
"every": "hour",
|
||||
"reload": true
|
||||
},
|
||||
{
|
||||
"name": "bunkernet-data",
|
||||
"file": "bunkernet-data.py",
|
||||
"every": "hour",
|
||||
"reload": true
|
||||
}
|
||||
]
|
||||
}
|
||||
4
core/clientcache/confs/http/client-cache.conf
Normal file
4
core/clientcache/confs/http/client-cache.conf
Normal file
@@ -0,0 +1,4 @@
|
||||
map $uri $cache_control {
|
||||
default "";
|
||||
"~\.({{ CLIENT_CACHE_EXTENSIONS }})$" "{{ CLIENT_CACHE_CONTROL }}";
|
||||
}
|
||||
8
core/clientcache/confs/server-http/client-cache.conf
Normal file
8
core/clientcache/confs/server-http/client-cache.conf
Normal file
@@ -0,0 +1,8 @@
|
||||
{% if USE_CLIENT_CACHE == "yes" +%}
|
||||
add_header Cache-Control $cache_control;
|
||||
{% if CLIENT_CACHE_ETAG == "yes" and SERVE_FILES == "yes" and USE_REVERSE_PROXY == "no" +%}
|
||||
etag on;
|
||||
{% else +%}
|
||||
etag off;
|
||||
{% endif +%}
|
||||
{% endif %}
|
||||
45
core/clientcache/plugin.json
Normal file
45
core/clientcache/plugin.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"id": "clientcache",
|
||||
"order": 999,
|
||||
"name": "Client cache",
|
||||
"description": "Manage caching for clients.",
|
||||
"version": "0.1",
|
||||
"settings": {
|
||||
"USE_CLIENT_CACHE": {
|
||||
"context": "multisite",
|
||||
"default": "no",
|
||||
"help": "Tell client to store locally static files.",
|
||||
"id": "use-client-cache",
|
||||
"label": "Use client cache",
|
||||
"regex": "^(yes|no)$",
|
||||
"type": "check"
|
||||
},
|
||||
"CLIENT_CACHE_EXTENSIONS": {
|
||||
"context": "global",
|
||||
"default": "jpg|jpeg|png|bmp|ico|svg|tif|css|js|otf|ttf|eot|woff|woff2",
|
||||
"help": "List of file extensions that should be cached.",
|
||||
"id": "client-cache-extensions",
|
||||
"label": "Extensions that should be cached by the client",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"CLIENT_CACHE_ETAG": {
|
||||
"context": "multisite",
|
||||
"default": "yes",
|
||||
"help": "Send the HTTP ETag header for static resources.",
|
||||
"id": "client-cache-etag",
|
||||
"label": "ETag",
|
||||
"regex": "^(yes|no)$",
|
||||
"type": "check"
|
||||
},
|
||||
"CLIENT_CACHE_CONTROL": {
|
||||
"context": "multisite",
|
||||
"default": "public, max-age=15552000",
|
||||
"help": "Value of the Cache-Control HTTP header.",
|
||||
"id": "client-cache-control",
|
||||
"label": "Cache-Control header",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
108
core/country/country.lua
Normal file
108
core/country/country.lua
Normal file
@@ -0,0 +1,108 @@
|
||||
local _M = {}
|
||||
_M.__index = _M
|
||||
|
||||
local utils = require "utils"
|
||||
local datastore = require "datastore"
|
||||
local logger = require "logger"
|
||||
local cjson = require "cjson"
|
||||
|
||||
function _M.new()
|
||||
local self = setmetatable({}, _M)
|
||||
return self, nil
|
||||
end
|
||||
|
||||
function _M:access()
|
||||
-- Get variables
|
||||
local whitelist, err = utils.get_variable("WHITELIST_COUNTRY")
|
||||
if whitelist == nil then
|
||||
return false, err
|
||||
end
|
||||
local blacklist, err = utils.get_variable("BLACKLIST_COUNTRY")
|
||||
if blacklist == nil then
|
||||
return false, err
|
||||
end
|
||||
|
||||
-- Don't go further if nothing is enabled
|
||||
if whitelist == "" and blacklist == "" then
|
||||
return true, "country not activated"
|
||||
end
|
||||
|
||||
-- Check if IP is in cache
|
||||
local data, err = self:is_in_cache(ngx.var.remote_addr)
|
||||
if data then
|
||||
if data.result == "ok" then
|
||||
return true, "client IP " .. ngx.var.remote_addr .. " is in country cache (not blacklisted, country = " .. data.country .. ")", nil, nil
|
||||
end
|
||||
return true, "client IP " .. ngx.var.remote_addr .. " is in country cache (blacklisted, country = " .. data.country .. ")", true, ngx.HTTP_FORBIDDEN
|
||||
end
|
||||
|
||||
-- Don't go further if IP is not global
|
||||
local is_global, err = utils.ip_is_global(ngx.var.remote_addr)
|
||||
if is_global == nil then
|
||||
logger.log(ngx.ERR, "COUNTRY", "error while checking if ip is global : " .. err)
|
||||
elseif not is_global then
|
||||
self:add_to_cache(ngx.var.remote_addr, "unknown", "ok")
|
||||
return true, "client IP " .. ngx.var.remote_addr .. " is not global, skipping check", nil, nil
|
||||
end
|
||||
|
||||
-- Get the country of client
|
||||
local country, err = utils.get_country(ngx.var.remote_addr)
|
||||
if not country then
|
||||
return false, "can't get country of client IP " .. ngx.var.remote_addr .. " : " .. err, nil, nil
|
||||
end
|
||||
|
||||
-- Process whitelist first
|
||||
if whitelist ~= "" then
|
||||
for wh_country in whitelist:gmatch("%S+") do
|
||||
if wh_country == country then
|
||||
self:add_to_cache(ngx.var.remote_addr, country, "ok")
|
||||
return true, "client IP " .. ngx.var.remote_addr .. " is whitelisted (country = " .. country .. ")", nil, nil
|
||||
end
|
||||
end
|
||||
self:add_to_cache(ngx.var.remote_addr, country, "ko")
|
||||
return true, "client IP " .. ngx.var.remote_addr .. " is not whitelisted (country = " .. country .. ")", true, ngx.HTTP_FORBIDDEN
|
||||
end
|
||||
|
||||
-- And then blacklist
|
||||
if blacklist ~= "" then
|
||||
for bl_country in blacklist:gmatch("%S+") do
|
||||
if bl_country == country then
|
||||
self:add_to_cache(ngx.var.remote_addr, country, "ko")
|
||||
return true, "client IP " .. ngx.var.remote_addr .. " is blacklisted (country = " .. country .. ")", true, ngx.HTTP_FORBIDDEN
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Country IP is not in blacklist
|
||||
local ok, err = self:add_to_cache(ngx.var.remote_addr, country, "ok")
|
||||
if not ok then
|
||||
return false, "error while caching IP " .. ngx.var.remote_addr .. " : " .. err, false, nil
|
||||
end
|
||||
return true, "client IP " .. ngx.var.remote_addr .. " is not blacklisted (country = " .. country .. ")", nil, nil
|
||||
end
|
||||
|
||||
function _M:is_in_cache(ip)
|
||||
local data, err = datastore:get("plugin_country_cache_" .. ip)
|
||||
if not data then
|
||||
if err ~= "not found" then
|
||||
logger.log(ngx.ERR, "COUNTRY", "Error while accessing cache : " .. err)
|
||||
end
|
||||
return false, err
|
||||
end
|
||||
return cjson.decode(data), "success"
|
||||
end
|
||||
|
||||
function _M:add_to_cache(ip, country, result)
|
||||
local data = {
|
||||
country = country,
|
||||
result = result
|
||||
}
|
||||
local ok, err = datastore:set("plugin_country_cache_" .. ip, cjson.encode(data), 3600)
|
||||
if not ok then
|
||||
logger.log(ngx.ERR, "COUNTRY", "Error while adding ip to cache : " .. err)
|
||||
return false, err
|
||||
end
|
||||
return true, "success"
|
||||
end
|
||||
|
||||
return _M
|
||||
27
core/country/plugin.json
Normal file
27
core/country/plugin.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"id": "country",
|
||||
"order": 2,
|
||||
"name": "Country",
|
||||
"description": "Deny access based on the country of the client IP.",
|
||||
"version": "0.1",
|
||||
"settings": {
|
||||
"BLACKLIST_COUNTRY": {
|
||||
"context": "multisite",
|
||||
"default": "",
|
||||
"help": "Deny access if the country of the client is in the list (2 letters code).",
|
||||
"id": "country-blacklist",
|
||||
"label": "Country blacklist",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"WHITELIST_COUNTRY": {
|
||||
"context": "multisite",
|
||||
"default": "",
|
||||
"help": "Deny access if the country of the client is not in the list (2 letters code).",
|
||||
"id": "country-whitelist",
|
||||
"label": "Country whitelist",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
19
core/customcert/confs/server-http/custom-cert.conf
Normal file
19
core/customcert/confs/server-http/custom-cert.conf
Normal file
@@ -0,0 +1,19 @@
|
||||
{% if USE_CUSTOM_HTTPS == "yes" +%}
|
||||
|
||||
# listen on HTTPS PORT
|
||||
listen 0.0.0.0:{{ HTTPS_PORT }} ssl {% if HTTP2 == "yes" %}http2{% endif %} {% if USE_PROXY_PROTOCOL == "yes" %}proxy_protocol{% endif %};
|
||||
|
||||
# TLS config
|
||||
ssl_certificate {{ CUSTOM_HTTPS_CERT }};
|
||||
ssl_certificate_key {{ CUSTOM_HTTPS_KEY }};
|
||||
ssl_protocols {{ HTTPS_PROTOCOLS }};
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_session_tickets off;
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:MozSSL:10m;
|
||||
{% if "TLSv1.2" in HTTPS_PROTOCOLS +%}
|
||||
ssl_dhparam /etc/nginx/dhparam;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
66
core/customcert/jobs/custom-cert.py
Normal file
66
core/customcert/jobs/custom-cert.py
Normal file
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import sys, os, subprocess, traceback
|
||||
|
||||
sys.path.append("/opt/bunkerweb/deps/python")
|
||||
sys.path.append("/opt/bunkerweb/utils")
|
||||
|
||||
import logger, jobs
|
||||
|
||||
def check_cert(cert_path) :
|
||||
try :
|
||||
cache_path = "/opt/bunkerweb/cache/customcert/" + cert_path.replace("/", "_") + ".hash"
|
||||
current_hash = jobs.file_hash(cert_path)
|
||||
if not os.path.isfile(cache_path) :
|
||||
with open(cache_path, "w") as f :
|
||||
f.write(current_hash)
|
||||
old_hash = jobs.file_hash(cache_path)
|
||||
if old_hash == current_hash :
|
||||
return False
|
||||
with open(cache_path, "w") as f :
|
||||
f.write(current_hash)
|
||||
return True
|
||||
except :
|
||||
logger.log("CUSTOM-CERT", "❌", "Exception while running custom-cert.py (check_cert) :")
|
||||
print(traceback.format_exc())
|
||||
return False
|
||||
|
||||
status = 0
|
||||
|
||||
try :
|
||||
|
||||
os.makedirs("/opt/bunkerweb/cache/customcert/", exist_ok=True)
|
||||
|
||||
# Multisite case
|
||||
if os.getenv("MULTISITE") == "yes" :
|
||||
for first_server in os.getenv("SERVER_NAME").split(" ") :
|
||||
if os.getenv(first_server + "_USE_CUSTOM_HTTPS", os.getenv("USE_CUSTOM_HTTPS")) != "yes" :
|
||||
continue
|
||||
if first_server == "" :
|
||||
continue
|
||||
cert_path = os.getenv(first_server + "_CUSTOM_HTTPS_CERT")
|
||||
logger.log("CUSTOM-CERT", "ℹ️", "Checking if certificate " + cert_path + " changed ...")
|
||||
need_reload = check_cert(cert_path)
|
||||
if need_reload :
|
||||
logger.log("CUSTOM-CERT", "ℹ️", "Detected change for certificate " + cert_path)
|
||||
status = 1
|
||||
else :
|
||||
logger.log("CUSTOM-CERT", "ℹ️", "No change for certificate " + cert_path)
|
||||
|
||||
# Singlesite case
|
||||
elif os.getenv("USE_CUSTOM_HTTPS") == "yes" and os.getenv("SERVER_NAME") != "" :
|
||||
cert_path = os.getenv("CUSTOM_HTTPS_CERT")
|
||||
logger.log("CUSTOM-CERT", "ℹ️", "Checking if certificate " + cert_path + " changed ...")
|
||||
need_reload = check_cert(cert_path)
|
||||
if need_reload :
|
||||
logger.log("CUSTOM-CERT", "ℹ️", "Detected change for certificate " + cert_path)
|
||||
status = 1
|
||||
else :
|
||||
logger.log("CUSTOM-CERT", "ℹ️", "No change for certificate " + cert_path)
|
||||
|
||||
except :
|
||||
status = 2
|
||||
logger.log("CUSTOM-CERT", "❌", "Exception while running custom-cert.py :")
|
||||
print(traceback.format_exc())
|
||||
|
||||
sys.exit(status)
|
||||
44
core/customcert/plugin.json
Normal file
44
core/customcert/plugin.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"id": "customcert",
|
||||
"order": 999,
|
||||
"name": "Custom HTTPS certificate",
|
||||
"description": "Choose custom certificate for HTTPS.",
|
||||
"version": "0.1",
|
||||
"settings": {
|
||||
"USE_CUSTOM_HTTPS": {
|
||||
"context": "multisite",
|
||||
"default": "no",
|
||||
"help": "Use custom HTTPS certificate.",
|
||||
"id": "use-custom-https",
|
||||
"label": "Use custom certificate",
|
||||
"regex": "^(yes|no)$",
|
||||
"type": "check"
|
||||
},
|
||||
"CUSTOM_HTTPS_CERT": {
|
||||
"context": "multisite",
|
||||
"default": "",
|
||||
"help": "Full path of the certificate or bundle file.",
|
||||
"id": "custom-https-cert",
|
||||
"label": "Certificate path",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"CUSTOM_HTTPS_KEY": {
|
||||
"context": "multisite",
|
||||
"default": "",
|
||||
"help": "Full path of the key file.",
|
||||
"id": "custom-https-key",
|
||||
"label": "Key path",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
}
|
||||
},
|
||||
"jobs": [
|
||||
{
|
||||
"name": "custom-cert",
|
||||
"file": "custom-cert.py",
|
||||
"every": "day",
|
||||
"reload": true
|
||||
}
|
||||
]
|
||||
}
|
||||
136
core/dnsbl/dnsbl.lua
Normal file
136
core/dnsbl/dnsbl.lua
Normal file
@@ -0,0 +1,136 @@
|
||||
local _M = {}
|
||||
_M.__index = _M
|
||||
|
||||
local utils = require "utils"
|
||||
local datastore = require "datastore"
|
||||
local logger = require "logger"
|
||||
local cjson = require "cjson"
|
||||
local resolver = require "resty.dns.resolver"
|
||||
|
||||
function _M.new()
|
||||
local self = setmetatable({}, _M)
|
||||
return self, nil
|
||||
end
|
||||
|
||||
function _M:init()
|
||||
-- Check if init is needed
|
||||
local init_needed, err = utils.has_variable("USE_DNSBL", "yes")
|
||||
if init_needed == nil then
|
||||
return false, "can't check USE_DNS variable : " .. err
|
||||
end
|
||||
if not init_needed then
|
||||
return true, "no service uses Blacklist, skipping init"
|
||||
end
|
||||
-- Read DNSBL list
|
||||
local str_dnsbls, err = utils.get_variable("DNSBL_LIST", false)
|
||||
if not str_dnsbls then
|
||||
return false, "can't get DNSBL_LIST variable : " .. err
|
||||
end
|
||||
local dnsbls = {}
|
||||
local i = 0
|
||||
for dnsbl in str_dnsbls:gmatch("%S+") do
|
||||
table.insert(dnsbls, dnsbl)
|
||||
i = i + 1
|
||||
end
|
||||
-- Load it into datastore
|
||||
local ok, err = datastore:set("plugin_dnsbl_list", cjson.encode(dnsbls))
|
||||
if not ok then
|
||||
return false, "can't store DNSBL list into datastore : " .. err
|
||||
end
|
||||
return true, "successfully loaded " .. tostring(i) .. " DNSBL server(s)"
|
||||
end
|
||||
|
||||
function _M:access()
|
||||
-- Check if access is needed
|
||||
local access_needed, err = utils.get_variable("USE_DNSBL")
|
||||
if access_needed == nil then
|
||||
return false, err
|
||||
end
|
||||
if access_needed ~= "yes" then
|
||||
return true, "DNSBL not activated"
|
||||
end
|
||||
|
||||
-- Check if IP is in cache
|
||||
local dnsbl, err = self:is_in_cache(ngx.var.remote_addr)
|
||||
if dnsbl then
|
||||
if dnsbl == "ok" then
|
||||
return true, "client IP " .. ngx.var.remote_addr .. " is in DNSBL cache (not blacklisted)", nil, nil
|
||||
end
|
||||
return true, "client IP " .. ngx.var.remote_addr .. " is in DNSBL cache (server = " .. dnsbl .. ")", true, ngx.HTTP_FORBIDDEN
|
||||
end
|
||||
|
||||
-- Don't go further if IP is not global
|
||||
local is_global, err = utils.ip_is_global(ngx.var.remote_addr)
|
||||
if is_global == nil then
|
||||
return false, "can't check if client IP is global : " .. err, nil, nil
|
||||
end
|
||||
if not utils.ip_is_global(ngx.var.remote_addr) then
|
||||
self:add_to_cache(ngx.var.remote_addr, "ok")
|
||||
return true, "client IP is not global, skipping DNSBL check", nil, nil
|
||||
end
|
||||
|
||||
-- Get list
|
||||
local data, err = datastore:get("plugin_dnsbl_list")
|
||||
if not data then
|
||||
return false, "can't get DNSBL list : " .. err, false, nil
|
||||
end
|
||||
local ok, dnsbls = pcall(cjson.decode, data)
|
||||
if not ok then
|
||||
return false, "error while decoding DNSBL list : " .. dnsbls, false, nil
|
||||
end
|
||||
|
||||
-- Loop on dnsbl list
|
||||
for i, dnsbl in ipairs(dnsbls) do
|
||||
local result, err = self:is_in_dnsbl(dnsbl, ngx.var.remote_addr)
|
||||
if result then
|
||||
self:add_to_cache(ngx.var.remote_addr, dnsbl)
|
||||
return ret, "client IP " .. ngx.var.remote_addr .. " is in DNSBL (server = " .. dnsbl .. ")", true, ngx.HTTP_FORBIDDEN
|
||||
end
|
||||
end
|
||||
|
||||
-- IP is not in DNSBL
|
||||
local ok, err = self:add_to_cache(ngx.var.remote_addr, "ok")
|
||||
if not ok then
|
||||
return false, "IP is not in DNSBL (error = " .. err .. ")", false, nil
|
||||
end
|
||||
return true, "IP is not in DNSBL", false, nil
|
||||
|
||||
end
|
||||
|
||||
function _M:is_in_dnsbl(dnsbl, ip)
|
||||
local request = resolver.arpa_str(ip) .. "." .. dnsbl
|
||||
local ips, err = utils.get_ips(request)
|
||||
if not ips then
|
||||
logger.log(ngx.ERR, "DNSBL", "Error while asking DNSBL server " .. dnsbl .. " : " .. err)
|
||||
return false, err
|
||||
end
|
||||
for i, ip in ipairs(ips) do
|
||||
local a, b, c, d = ip:match("([%d]+).([%d]+).([%d]+).([%d]+)")
|
||||
if a == "127" then
|
||||
return true, "success"
|
||||
end
|
||||
end
|
||||
return false, "success"
|
||||
end
|
||||
|
||||
function _M:is_in_cache(ip)
|
||||
local kind, err = datastore:get("plugin_dnsbl_cache_" .. ip)
|
||||
if not kind then
|
||||
if err ~= "not found" then
|
||||
logger.log(ngx.ERR, "DNSBL", "Error while accessing cache : " .. err)
|
||||
end
|
||||
return false, err
|
||||
end
|
||||
return kind, "success"
|
||||
end
|
||||
|
||||
function _M:add_to_cache(ip, kind)
|
||||
local ok, err = datastore:set("plugin_dnsbl_cache_" .. ip, kind, 3600)
|
||||
if not ok then
|
||||
logger.log(ngx.ERR, "DNSBL", "Error while adding ip to cache : " .. err)
|
||||
return false, err
|
||||
end
|
||||
return true, "success"
|
||||
end
|
||||
|
||||
return _M
|
||||
27
core/dnsbl/plugin.json
Normal file
27
core/dnsbl/plugin.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"id": "dnsbl",
|
||||
"order": 2,
|
||||
"name": "DNSBL",
|
||||
"description": "Deny access based on external DNSBL servers.",
|
||||
"version": "0.1",
|
||||
"settings": {
|
||||
"USE_DNSBL": {
|
||||
"context": "multisite",
|
||||
"default": "yes",
|
||||
"help": "Activate DNSBL feature.",
|
||||
"id": "use-dnsbl",
|
||||
"label": "Activate DNSBL",
|
||||
"regex": "^(yes|no)$",
|
||||
"type": "check"
|
||||
},
|
||||
"DNSBL_LIST": {
|
||||
"context": "global",
|
||||
"default": "bl.blocklist.de problems.dnsbl.sorbs.net sbl.spamhaus.org xbl.spamhaus.org",
|
||||
"help": "List of DNSBL servers.",
|
||||
"id": "dnsbl-list",
|
||||
"label": "DNSBL list",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
47
core/errors/confs/server-http/errors.conf
Normal file
47
core/errors/confs/server-http/errors.conf
Normal file
@@ -0,0 +1,47 @@
|
||||
{% if ERRORS != "" %}
|
||||
{% for element in ERRORS.split(" ") %}
|
||||
{% set code = element.split("=")[0] %}
|
||||
{% set page = element.split("=")[1] %}
|
||||
error_page {{ code }} {{ page }};
|
||||
|
||||
location = {{ page }} {
|
||||
root {% if ROOT_FOLDER == "" %}/opt/bunkerweb/www/{% if MULTISITE == "yes" %}{{ SERVER_NAME.split(" ")[0] }}{% endif %}{% else %}{{ ROOT_FOLDER }}{% endif %};
|
||||
modsecurity off;
|
||||
internal;
|
||||
}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% set default_errors = ["400", "401", "403", "404", "405", "413", "429", "500", "501", "502", "503", "504"] %}
|
||||
|
||||
{% for default_error in default_errors %}
|
||||
{% if not default_error + "=" in ERRORS +%}
|
||||
{% if default_error == "405" +%}
|
||||
error_page 405 =200 @405;
|
||||
{% else +%}
|
||||
error_page {{ default_error }} @{{ default_error }};
|
||||
{% endif +%}
|
||||
location @{{ default_error }} {
|
||||
auth_basic off;
|
||||
internal;
|
||||
modsecurity off;
|
||||
default_type 'text/html';
|
||||
content_by_lua_block {
|
||||
local logger = require "logger"
|
||||
local errors = require "errors.errors"
|
||||
local html, err
|
||||
if ngx.status == 200 then
|
||||
html, err = errors.error_html(tostring(405))
|
||||
else
|
||||
html, err = errors.error_html(tostring(ngx.status))
|
||||
end
|
||||
if not html then
|
||||
logger.log(ngx.ERR, "ERRORS", "Error while computing HTML error template for {{ default_error }} : " .. err)
|
||||
else
|
||||
ngx.say(html)
|
||||
end
|
||||
}
|
||||
}
|
||||
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
124
core/errors/errors.lua
Normal file
124
core/errors/errors.lua
Normal file
@@ -0,0 +1,124 @@
|
||||
local _M = {}
|
||||
_M.__index = _M
|
||||
|
||||
local utils = require "utils"
|
||||
local datastore = require "datastore"
|
||||
local logger = require "logger"
|
||||
local cjson = require "cjson"
|
||||
|
||||
function _M.new()
|
||||
local self = setmetatable({}, _M)
|
||||
return self, nil
|
||||
end
|
||||
|
||||
function _M:init()
|
||||
-- Save default errors into datastore
|
||||
local default_errors = {
|
||||
["400"] = {
|
||||
title = "400 - Bad Request",
|
||||
body1 = "Bad Request",
|
||||
body2 = "400",
|
||||
body3 = "The server did not understand the request."
|
||||
},
|
||||
["401"] = {
|
||||
title = "401 - Not Authorized",
|
||||
body1 = "Not Authorized",
|
||||
body2 = "401",
|
||||
body3 = "Valid authentication credentials needed for the target resource."
|
||||
},
|
||||
["403"] = {
|
||||
title = "403 - Forbidden",
|
||||
body1 = "Forbidden",
|
||||
body2 = "403",
|
||||
body3 = "Access is forbidden to the requested page."
|
||||
},
|
||||
["404"] = {
|
||||
title = "404 - Not Found",
|
||||
body1 = "Not Found",
|
||||
body2 = "404",
|
||||
body3 = "The server cannot find the requested page."
|
||||
},
|
||||
["405"] = {
|
||||
title = "405 - Method Not Allowed",
|
||||
body1 = "Method Not Allowed",
|
||||
body2 = "405",
|
||||
body3 = "The method specified in the request is not allowed."
|
||||
},
|
||||
["413"] = {
|
||||
title = "413 - Request Entity Too Large",
|
||||
body1 = "Request Entity Too Large",
|
||||
body2 = "413",
|
||||
body3 = "The server will not accept the request, because the request entity is too large."
|
||||
},
|
||||
["429"] = {
|
||||
title = "429 - Too Many Requests",
|
||||
body1 = "Too Many Requests",
|
||||
body2 = "429",
|
||||
body3 = "Too many requests sent in a given amount of time, try again later."
|
||||
},
|
||||
["500"] = {
|
||||
title = "500 - Internal Server Error",
|
||||
body1 = "Internal Server Error",
|
||||
body2 = "500",
|
||||
body3 = "The request was not completed. The server met an unexpected condition."
|
||||
},
|
||||
["501"] = {
|
||||
title = "501 - Not Implemented",
|
||||
body1 = "Not Implemented",
|
||||
body2 = "501",
|
||||
body3 = "The request was not completed. The server did not support the functionality required."
|
||||
},
|
||||
["502"] = {
|
||||
title = "502 - Bad Gateway",
|
||||
body1 = "Bad Gateway",
|
||||
body2 = "502",
|
||||
body3 = "The request was not completed. The server received an invalid response from the upstream server."
|
||||
},
|
||||
["503"] = {
|
||||
title = "503 - Service Unavailable",
|
||||
body1 = "Service Unavailable",
|
||||
body2 = "503",
|
||||
body3 = "The request was not completed. The server is temporarily overloading or down."
|
||||
},
|
||||
["504"] = {
|
||||
title = "504 - Gateway Timeout",
|
||||
body1 = "Gateway Timeout",
|
||||
body2 = "504",
|
||||
body3 = "The gateway has timed out."
|
||||
}
|
||||
}
|
||||
local ok, err = datastore:set("plugin_errors_default_errors", cjson.encode(default_errors))
|
||||
if not ok then
|
||||
return false, "can't save default errors to datastore : " .. err
|
||||
end
|
||||
-- Save generic template into datastore
|
||||
local f, err = io.open("/opt/bunkerweb/core/errors/files/error.html", "r")
|
||||
if not f then
|
||||
return false, "can't open error.html : " .. err
|
||||
end
|
||||
local template = f:read("*all")
|
||||
f:close()
|
||||
local ok, err = datastore:set("plugin_errors_template", template)
|
||||
if not ok then
|
||||
return false, "can't save error.html to datastore : " .. err
|
||||
end
|
||||
return true, "success"
|
||||
end
|
||||
|
||||
function _M.error_html(code)
|
||||
-- Load default errors texts
|
||||
local default_errors, err = datastore:get("plugin_errors_default_errors")
|
||||
if not default_errors then
|
||||
return false, "can't get default errors from datastore : " .. err
|
||||
end
|
||||
default_errors = cjson.decode(default_errors)
|
||||
-- Load template
|
||||
local template, err = datastore:get("plugin_errors_template")
|
||||
if not template then
|
||||
return false, "can't get template from datastore : " .. err
|
||||
end
|
||||
-- Compute template
|
||||
return template:format(default_errors[code].title, default_errors[code].body1, default_errors[code].body2, default_errors[code].body3), "success"
|
||||
end
|
||||
|
||||
return _M
|
||||
27
core/errors/files/error.html
Normal file
27
core/errors/files/error.html
Normal file
File diff suppressed because one or more lines are too long
18
core/errors/plugin.json
Normal file
18
core/errors/plugin.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "errors",
|
||||
"order": 999,
|
||||
"name": "Errors",
|
||||
"description": "Manage default error pages",
|
||||
"version": "0.1",
|
||||
"settings": {
|
||||
"ERRORS": {
|
||||
"context": "multisite",
|
||||
"default": "",
|
||||
"help": "List of HTTP error code and corresponding error pages (404=/my404.html 403=/errors/403.html ...).",
|
||||
"id": "errors",
|
||||
"label": "Errors",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
6
core/gzip/confs/server-http/gzip.conf
Normal file
6
core/gzip/confs/server-http/gzip.conf
Normal file
@@ -0,0 +1,6 @@
|
||||
{% if USE_GZIP == "yes" +%}
|
||||
gzip on;
|
||||
gzip_types {{ GZIP_TYPES }};
|
||||
gzip_comp_level {{ GZIP_COMP_LEVEL }};
|
||||
gzip_min_length {{ GZIP_MIN_LENGTH }};
|
||||
{% endif %}
|
||||
56
core/gzip/plugin.json
Normal file
56
core/gzip/plugin.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"id": "gzip",
|
||||
"order": 999,
|
||||
"name": "Gzip",
|
||||
"description": "Compress HTTP requests with the gzip algorithm.",
|
||||
"version": "0.1",
|
||||
"settings": {
|
||||
"USE_GZIP": {
|
||||
"context": "multisite",
|
||||
"default": "no",
|
||||
"help": "Use gzip",
|
||||
"id": "use-gzip",
|
||||
"label": "Use gzip",
|
||||
"regex": "^(yes|no)$",
|
||||
"type": "check"
|
||||
},
|
||||
"GZIP_TYPES": {
|
||||
"context": "multisite",
|
||||
"default": "application/atom+xml application/javascript application/json application/rss+xml application/vnd.ms-fontobject application/x-font-opentype application/x-font-truetype application/x-font-ttf application/x-javascript application/xhtml+xml application/xml font/eot font/opentype font/otf font/truetype image/svg+xml image/vnd.microsoft.icon image/x-icon image/x-win-bitmap text/css text/javascript text/plain text/xml",
|
||||
"help": "List of MIME types that will be compressed with gzip.",
|
||||
"id": "gzip-types",
|
||||
"label": "MIME types",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"GZIP_MIN_LENGTH": {
|
||||
"context": "multisite",
|
||||
"default": "1000",
|
||||
"help": "Minimum length for gzip compression.",
|
||||
"id": "gzip-min-length",
|
||||
"label": "Minimum length",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"GZIP_COMP_LEVEL": {
|
||||
"context": "multisite",
|
||||
"default": "5",
|
||||
"help": "The compression level of the gzip algorithm.",
|
||||
"id": "gzip-comp-level",
|
||||
"label": "Compression level",
|
||||
"regex": "^[1-9]$",
|
||||
"type": "select",
|
||||
"select": [
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
4
core/headers/confs/http/headers.conf
Normal file
4
core/headers/confs/http/headers.conf
Normal file
@@ -0,0 +1,4 @@
|
||||
map $scheme $header_cookie_secure {
|
||||
default "";
|
||||
"https" "secure";
|
||||
}
|
||||
5
core/headers/confs/server-http/custom-headers.conf
Normal file
5
core/headers/confs/server-http/custom-headers.conf
Normal file
@@ -0,0 +1,5 @@
|
||||
{% for k, v in all.items() +%}
|
||||
{% if k.startswith("CUSTOM_HEADER") and v != "" +%}
|
||||
more_set_headers "{{ v }}";
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
5
core/headers/confs/server-http/remove-headers.conf
Normal file
5
core/headers/confs/server-http/remove-headers.conf
Normal file
@@ -0,0 +1,5 @@
|
||||
{% if REMOVE_HEADERS != "" %}
|
||||
{% for header in REMOVE_HEADERS.split(" ") +%}
|
||||
more_clear_headers '{{ header }}';
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
39
core/headers/confs/server-http/security-headers.conf
Normal file
39
core/headers/confs/server-http/security-headers.conf
Normal file
@@ -0,0 +1,39 @@
|
||||
{% if STRICT_TRANSPORT_SECURITY != "" and (AUTO_LETS_ENCRYPT == "yes" or USE_CUSTOM_HTTPS == "yes" or GENERATE_SELF_SIGNED_SSL == "yes") +%}
|
||||
more_set_headers "Strict-Transport-Security: {{ STRICT_TRANSPORT_SECURITY }}";
|
||||
{% endif +%}
|
||||
|
||||
{% if COOKIE_FLAGS != "" +%}
|
||||
{% if COOKIE_AUTO_SECURE_FLAG == "yes" and (AUTO_LETS_ENCRYPT == "yes" or USE_CUSTOM_HTTPS == "yes" or GENERATE_SELF_SIGNED_SSL == "yes") +%}
|
||||
set_cookie_flag {{ COOKIE_FLAGS }} secure;
|
||||
{% else +%}
|
||||
set_cookie_flag {{ COOKIE_FLAGS }};
|
||||
{% endif +%}
|
||||
{% endif +%}
|
||||
|
||||
{% if CONTENT_SECURITY_POLICY != "" +%}
|
||||
more_set_headers "Content-Security-Policy: {{ CONTENT_SECURITY_POLICY }}";
|
||||
{% endif +%}
|
||||
|
||||
{% if REFERRER_POLICY != "" +%}
|
||||
more_set_headers "Referrer-Policy: {{ REFERRER_POLICY }}";
|
||||
{% endif +%}
|
||||
|
||||
{% if PERMISSIONS_POLICY != "" +%}
|
||||
more_set_headers "Permissions-Policy: {{ PERMISSIONS_POLICY }}";
|
||||
{% endif +%}
|
||||
|
||||
{% if FEATURE_POLICY != "" +%}
|
||||
more_set_headers "Feature-Policy: {{ FEATURE_POLICY }}";
|
||||
{% endif +%}
|
||||
|
||||
{% if X_FRAME_OPTIONS != "" +%}
|
||||
more_set_headers "X-Frame-Options: {{ X_FRAME_OPTIONS }}";
|
||||
{% endif +%}
|
||||
|
||||
{% if X_CONTENT_TYPE_OPTIONS != "" +%}
|
||||
more_set_headers "X-Content-Type-Options: {{ X_CONTENT_TYPE_OPTIONS }}";
|
||||
{% endif +%}
|
||||
|
||||
{% if X_XSS_PROTECTION != "" +%}
|
||||
more_set_headers "X-XSS-Protection: {{ X_XSS_PROTECTION }}";
|
||||
{% endif +%}
|
||||
118
core/headers/plugin.json
Normal file
118
core/headers/plugin.json
Normal file
@@ -0,0 +1,118 @@
|
||||
{
|
||||
"id": "headers",
|
||||
"order": 999,
|
||||
"name": "Headers",
|
||||
"description": "Manage HTTP headers sent to clients.",
|
||||
"version": "0.1",
|
||||
"settings": {
|
||||
"CUSTOM_HEADER": {
|
||||
"context": "multisite",
|
||||
"default": "",
|
||||
"help": "Custom header to add (HeaderName: HeaderValue).",
|
||||
"id": "custom-header",
|
||||
"label": "Custom header (HeaderName: HeaderValue)",
|
||||
"regex": "^.*$",
|
||||
"type": "text",
|
||||
"multiple": "custom-headers"
|
||||
},
|
||||
"REMOVE_HEADERS": {
|
||||
"context": "multisite",
|
||||
"default": "Server X-Powered-By X-AspNet-Version X-AspNetMvc-Version",
|
||||
"help": "Headers to remove (Header1 Header2 Header3 ...)",
|
||||
"id": "remove-headers",
|
||||
"label": "Remove headers",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"STRICT_TRANSPORT_SECURITY": {
|
||||
"context": "multisite",
|
||||
"default": "max-age=31536000",
|
||||
"help": "Value for the Strict-Transport-Security header.",
|
||||
"id": "strict-transport-security",
|
||||
"label": "Strict-Transport-Security",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"COOKIE_FLAGS": {
|
||||
"context": "multisite",
|
||||
"default": "* HttpOnly SameSite=Lax",
|
||||
"help": "Cookie flags automatically added to all cookies (value accepted for nginx_cookie_flag_module).",
|
||||
"id": "cookie-flags",
|
||||
"label": "Cookie flags",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"COOKIE_AUTO_SECURE_FLAG": {
|
||||
"context": "multisite",
|
||||
"default": "yes",
|
||||
"help": "Automatically add the Secure flag to all cookies.",
|
||||
"id": "cookie-auto-secure-flag",
|
||||
"label": "Cookie auto Secure flag",
|
||||
"regex": "^(yes|no)$",
|
||||
"type": "check"
|
||||
},
|
||||
"CONTENT_SECURITY_POLICY": {
|
||||
"context": "multisite",
|
||||
"default": "object-src 'none'; form-action 'self'; frame-ancestors 'self';",
|
||||
"help": "Value for the Content-Security-Policy header.",
|
||||
"id": "content-security-policy",
|
||||
"label": "Content-Security-Policy",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"REFERRER_POLICY": {
|
||||
"context": "multisite",
|
||||
"default": "strict-origin-when-cross-origin",
|
||||
"help": "Value for the Referrer-Policy header.",
|
||||
"id": "referrer-policy",
|
||||
"label": "Referrer-Policy",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"PERMISSIONS_POLICY": {
|
||||
"context": "multisite",
|
||||
"default": "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), hid=(), idle-detection=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), serial=(), usb=(), web-share=(), xr-spatial-tracking=()",
|
||||
"help": "Value for the Permissions-Policy header.",
|
||||
"id": "permissions-policy",
|
||||
"label": "Permissions-Policy",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"FEATURE_POLICY": {
|
||||
"context": "multisite",
|
||||
"default": "accelerometer 'none'; ambient-light-sensor 'none'; autoplay 'none'; battery 'none'; camera 'none'; display-capture 'none'; document-domain 'none'; encrypted-media 'none'; execution-while-not-rendered 'none'; execution-while-out-of-viewport 'none'; fullscreen 'none'; 'none'; geolocation 'none'; gyroscope 'none'; layout-animation 'none'; legacy-image-formats 'none'; magnetometer 'none'; microphone 'none'; midi 'none'; navigation-override 'none'; payment 'none'; picture-in-picture 'none'; publickey-credentials-get 'none'; speaker-selection 'none'; sync-xhr 'none'; unoptimized-images 'none'; unsized-media 'none'; usb 'none'; screen-wake-lock 'none'; web-share 'none'; xr-spatial-tracking 'none';",
|
||||
"help": "Value for the Feature-Policy header.",
|
||||
"id": "feature-policy",
|
||||
"label": "Feature-Policy",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"X_FRAME_OPTIONS": {
|
||||
"context": "multisite",
|
||||
"default": "SAMEORIGIN",
|
||||
"help": "Value for the X-Frame-Options header.",
|
||||
"id": "x-frame-options",
|
||||
"label": "X-Frame-Options",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"X_CONTENT_TYPE_OPTIONS": {
|
||||
"context": "multisite",
|
||||
"default": "nosniff",
|
||||
"help": "Value for the X-Content-Type-Options header.",
|
||||
"id": "x-content-type-options",
|
||||
"label": "X-Content-Type-Options",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"X_XSS_PROTECTION": {
|
||||
"context": "multisite",
|
||||
"default": "1; mode=block",
|
||||
"help": "Value for the X-XSS-Protection header.",
|
||||
"id": "x-xss-protection",
|
||||
"label": "X-XSS-Protection",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
3
core/inject/confs/server-http/inject.conf
Normal file
3
core/inject/confs/server-http/inject.conf
Normal file
@@ -0,0 +1,3 @@
|
||||
{% if INJECT_BODY != "" +%}
|
||||
sub_filter '</body>' '{{ INJECT_BODY }}</body>';
|
||||
{% endif %}
|
||||
18
core/inject/plugin.json
Normal file
18
core/inject/plugin.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "inject",
|
||||
"order": 999,
|
||||
"name": "HTML injection",
|
||||
"description": "Inject custom HTML code before the </body> tag.",
|
||||
"version": "0.1",
|
||||
"settings": {
|
||||
"INJECT_BODY": {
|
||||
"context": "multisite",
|
||||
"default": "",
|
||||
"help": "The HTML code to inject.",
|
||||
"id": "inject-body",
|
||||
"label": "HTML code",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
62
core/jobs/jobs/mmdb-asn.py
Executable file
62
core/jobs/jobs/mmdb-asn.py
Executable file
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import sys, os, traceback
|
||||
|
||||
sys.path.append("/opt/bunkerweb/deps/python")
|
||||
sys.path.append("/opt/bunkerweb/utils")
|
||||
|
||||
import logger, jobs
|
||||
import requests, datetime, gzip, maxminddb
|
||||
|
||||
status = 0
|
||||
|
||||
try :
|
||||
|
||||
# Don't go further if the cache is fresh
|
||||
if jobs.is_cached_file("/opt/bunkerweb/cache/asn.mmdb", "month") :
|
||||
logger.log("JOBS", "ℹ️", "asn.mmdb is already in cache, skipping download...")
|
||||
os._exit(0)
|
||||
|
||||
# Compute the mmdb URL
|
||||
today = datetime.date.today()
|
||||
mmdb_url = "https://download.db-ip.com/free/dbip-asn-lite-{}-{}.mmdb.gz".format(today.strftime("%Y"), today.strftime("%m"))
|
||||
|
||||
# Download the mmdb file
|
||||
logger.log("JOBS", "ℹ️", "Downloading mmdb file from url " + mmdb_url + " ...")
|
||||
resp = requests.get(mmdb_url)
|
||||
|
||||
# Save it to temp
|
||||
logger.log("JOBS", "ℹ️", "Saving mmdb file to tmp ...")
|
||||
with open("/opt/bunkerweb/tmp/asn.mmdb", "wb") as f :
|
||||
f.write(gzip.decompress(resp.content))
|
||||
|
||||
# Try to load it
|
||||
logger.log("JOBS", "ℹ️", "Checking if mmdb file is valid ...")
|
||||
with maxminddb.open_database("/opt/bunkerweb/tmp/asn.mmdb") as reader :
|
||||
pass
|
||||
|
||||
# Check if file has changed
|
||||
file_hash = jobs.file_hash("/opt/bunkerweb/tmp/asn.mmdb")
|
||||
cache_hash = jobs.cache_hash("/opt/bunkerweb/cache/asn.mmdb")
|
||||
if file_hash == cache_hash :
|
||||
logger.log("JOBS", "ℹ️", "New file is identical to cache file, reload is not needed")
|
||||
os._exit(0)
|
||||
|
||||
# Move it to cache folder
|
||||
logger.log("JOBS", "ℹ️", "Moving mmdb file to cache ...")
|
||||
cached, err = jobs.cache_file("/opt/bunkerweb/tmp/asn.mmdb", "/opt/bunkerweb/cache/asn.mmdb", file_hash)
|
||||
if not cached :
|
||||
logger.log("JOBS", "❌", "Error while caching mmdb file : " + err)
|
||||
os._exit(2)
|
||||
|
||||
# Success
|
||||
logger.log("JOBS", "ℹ️", "Downloaded new mmdb from " + mmdb_url)
|
||||
|
||||
status = 1
|
||||
|
||||
except :
|
||||
status = 2
|
||||
logger.log("JOBS", "❌", "Exception while running mmdb-asn.py :")
|
||||
print(traceback.format_exc())
|
||||
|
||||
sys.exit(status)
|
||||
62
core/jobs/jobs/mmdb-country.py
Executable file
62
core/jobs/jobs/mmdb-country.py
Executable file
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import sys, os, traceback
|
||||
|
||||
sys.path.append("/opt/bunkerweb/deps/python")
|
||||
sys.path.append("/opt/bunkerweb/utils")
|
||||
|
||||
import logger, jobs
|
||||
import requests, datetime, gzip, maxminddb
|
||||
|
||||
status = 0
|
||||
|
||||
try :
|
||||
|
||||
# Don't go further if the cache is fresh
|
||||
if jobs.is_cached_file("/opt/bunkerweb/cache/country.mmdb", "month") :
|
||||
logger.log("JOBS", "ℹ️", "country.mmdb is already in cache, skipping download...")
|
||||
os._exit(0)
|
||||
|
||||
# Compute the mmdb URL
|
||||
today = datetime.date.today()
|
||||
mmdb_url = "https://download.db-ip.com/free/dbip-country-lite-{}-{}.mmdb.gz".format(today.strftime("%Y"), today.strftime("%m"))
|
||||
|
||||
# Download the mmdb file
|
||||
logger.log("JOBS", "ℹ️", "Downloading mmdb file from url " + mmdb_url + " ...")
|
||||
resp = requests.get(mmdb_url)
|
||||
|
||||
# Save it to temp
|
||||
logger.log("JOBS", "ℹ️", "Saving mmdb file to tmp ...")
|
||||
with open("/opt/bunkerweb/tmp/country.mmdb", "wb") as f :
|
||||
f.write(gzip.decompress(resp.content))
|
||||
|
||||
# Try to load it
|
||||
logger.log("JOBS", "ℹ️", "Checking if mmdb file is valid ...")
|
||||
with maxminddb.open_database("/opt/bunkerweb/tmp/country.mmdb") as reader :
|
||||
pass
|
||||
|
||||
# Check if file has changed
|
||||
file_hash = jobs.file_hash("/opt/bunkerweb/tmp/country.mmdb")
|
||||
cache_hash = jobs.cache_hash("/opt/bunkerweb/cache/country.mmdb")
|
||||
if file_hash == cache_hash :
|
||||
logger.log("JOBS", "ℹ️", "New file is identical to cache file, reload is not needed")
|
||||
os._exit(0)
|
||||
|
||||
# Move it to cache folder
|
||||
logger.log("JOBS", "ℹ️", "Moving mmdb file to cache ...")
|
||||
cached, err = jobs.cache_file("/opt/bunkerweb/tmp/country.mmdb", "/opt/bunkerweb/cache/country.mmdb", file_hash)
|
||||
if not cached :
|
||||
logger.log("JOBS", "❌", "Error while caching mmdb file : " + err)
|
||||
os._exit(2)
|
||||
|
||||
# Success
|
||||
logger.log("JOBS", "ℹ️", "Downloaded new mmdb from " + mmdb_url)
|
||||
|
||||
status = 1
|
||||
|
||||
except :
|
||||
status = 2
|
||||
logger.log("JOBS", "❌", "Exception while running mmdb-country.py :")
|
||||
print(traceback.format_exc())
|
||||
|
||||
sys.exit(status)
|
||||
23
core/jobs/plugin.json
Normal file
23
core/jobs/plugin.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"id": "jobs",
|
||||
"order": 999,
|
||||
"name": "Jobs",
|
||||
"description": "Fake core plugin for internal jobs.",
|
||||
"version": "0.1",
|
||||
"settings": {
|
||||
},
|
||||
"jobs": [
|
||||
{
|
||||
"name": "mmdb-country",
|
||||
"file": "mmdb-country.py",
|
||||
"every": "week",
|
||||
"reload": true
|
||||
},
|
||||
{
|
||||
"name": "mmdb-asn",
|
||||
"file": "mmdb-asn.py",
|
||||
"every": "week",
|
||||
"reload": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# set location for challenges
|
||||
location ~ ^/.well-known/acme-challenge/ {
|
||||
root /opt/bunkerweb/tmp/lets-encrypt;
|
||||
}
|
||||
24
core/letsencrypt/confs/server-http/lets-encrypt.conf
Normal file
24
core/letsencrypt/confs/server-http/lets-encrypt.conf
Normal file
@@ -0,0 +1,24 @@
|
||||
# set location for challenges
|
||||
location ~ ^/.well-known/acme-challenge/ {
|
||||
root /opt/bunkerweb/tmp/lets-encrypt;
|
||||
}
|
||||
|
||||
{% if AUTO_LETS_ENCRYPT == "yes" %}
|
||||
|
||||
# listen on HTTPS PORT
|
||||
listen 0.0.0.0:{{ HTTPS_PORT }} ssl {% if HTTP2 == "yes" %}http2{% endif %} {% if USE_PROXY_PROTOCOL == "yes" %}proxy_protocol{% endif %};
|
||||
|
||||
# TLS config
|
||||
ssl_certificate /etc/letsencrypt/live/{{ SERVER_NAME.split(" ")[0] }}/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/{{ SERVER_NAME.split(" ")[0] }}/privkey.pem;
|
||||
ssl_protocols {{ HTTPS_PROTOCOLS }};
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_session_tickets off;
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:MozSSL:10m;
|
||||
{% if "TLSv1.2" in HTTPS_PROTOCOLS +%}
|
||||
ssl_dhparam /etc/nginx/dhparam;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
51
core/letsencrypt/jobs/certbot-auth.py
Executable file
51
core/letsencrypt/jobs/certbot-auth.py
Executable file
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import sys, os, traceback
|
||||
|
||||
sys.path.append("/opt/bunkerweb/deps/python")
|
||||
sys.path.append("/opt/bunkerweb/utils")
|
||||
sys.path.append("/opt/bunkerweb/api")
|
||||
|
||||
from logger import log
|
||||
from API import API
|
||||
|
||||
status = 0
|
||||
|
||||
try :
|
||||
# Get env vars
|
||||
is_kubernetes_mode = os.getenv("KUBERNETES_MODE") == "yes"
|
||||
is_swarm_mode = os.getenv("SWARM_MODE") == "yes"
|
||||
token = os.getenv("CERTBOT_TOKEN")
|
||||
validation = os.getenv("CERTBOT_VALIDATION")
|
||||
|
||||
# Cluster case
|
||||
if is_kubernetes_mode or is_swarm_mode :
|
||||
for variable, value in os.environ.items() :
|
||||
if not variable.startswith("CLUSTER_INSTANCE_") :
|
||||
continue
|
||||
endpoint = value.split(" ")[0]
|
||||
host = value.split(" ")[1]
|
||||
api = API(endpoint, host=host)
|
||||
sent, err, status, resp = api.request("POST", "/lets-encrypt/challenge", data={"token": token, "validation": validation})
|
||||
if not sent :
|
||||
status = 1
|
||||
log("LETS-ENCRYPT", "❌", "Can't send API request to " + api.get_endpoint() + "/lets-encrypt/challenge : " + err)
|
||||
else :
|
||||
if status != 200 :
|
||||
status = 1
|
||||
log("LETS-ENCRYPT", "❌", "Error while sending API request to " + api.get_endpoint() + "/lets-encrypt/challenge : status = " + resp["status"] + ", msg = " + resp["msg"])
|
||||
else :
|
||||
log("LETS-ENCRYPT", "ℹ️", "Successfully sent API request to " + api.get_endpoint() + "/lets-encrypt/challenge")
|
||||
|
||||
# Docker or Linux case
|
||||
else :
|
||||
root_dir = "/opt/bunkerweb/tmp/lets-encrypt/.well-known/acme-challenge/"
|
||||
os.makedirs(root_dir, exist_ok=True)
|
||||
with open(root_dir + token, "w") as f :
|
||||
f.write(validation)
|
||||
except :
|
||||
status = 1
|
||||
log("LETS-ENCRYPT", "❌", "Exception while running certbot-auth.py :")
|
||||
print(traceback.format_exc())
|
||||
|
||||
sys.exit(status)
|
||||
49
core/letsencrypt/jobs/certbot-cleanup.py
Executable file
49
core/letsencrypt/jobs/certbot-cleanup.py
Executable file
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import sys, os, traceback
|
||||
|
||||
sys.path.append("/opt/bunkerweb/deps/python")
|
||||
sys.path.append("/opt/bunkerweb/utils")
|
||||
sys.path.append("/opt/bunkerweb/api")
|
||||
|
||||
from logger import log
|
||||
from API import API
|
||||
|
||||
status = 0
|
||||
|
||||
try :
|
||||
# Get env vars
|
||||
is_kubernetes_mode = os.getenv("KUBERNETES_MODE") == "yes"
|
||||
is_swarm_mode = os.getenv("SWARM_MODE") == "yes"
|
||||
token = os.getenv("CERTBOT_TOKEN")
|
||||
|
||||
# Cluster case
|
||||
if is_kubernetes_mode or is_swarm_mode :
|
||||
for variable, value in os.environ.items() :
|
||||
if not variable.startswith("CLUSTER_INSTANCE_") :
|
||||
continue
|
||||
endpoint = value.split(" ")[0]
|
||||
host = value.split(" ")[1]
|
||||
api = API(endpoint, host=host)
|
||||
sent, err, status, resp = api.request("DELETE", "/lets-encrypt/challenge", data={"token": token})
|
||||
if not sent :
|
||||
status = 1
|
||||
log("LETS-ENCRYPT", "❌", "Can't send API request to " + api.get_endpoint() + "/lets-encrypt/challenge : " + err)
|
||||
else :
|
||||
if status != 200 :
|
||||
status = 1
|
||||
log("LETS-ENCRYPT", "❌", "Error while sending API request to " + api.get_endpoint() + "/lets-encrypt/challenge : status = " + resp["status"] + ", msg = " + resp["msg"])
|
||||
else :
|
||||
log("LETS-ENCRYPT", "ℹ️", "Successfully sent API request to " + api.get_endpoint() + "/lets-encrypt/challenge")
|
||||
|
||||
# Docker or Linux case
|
||||
else :
|
||||
challenge_path = "/opt/bunkerweb/tmp/lets-encrypt/.well-known/acme-challenge/" + token
|
||||
if os.path.isfile(challenge_path) :
|
||||
os.remove(challenge_path)
|
||||
except :
|
||||
status = 1
|
||||
log("LETS-ENCRYPT", "❌", "Exception while running certbot-cleanup.py :")
|
||||
print(traceback.format_exc())
|
||||
|
||||
sys.exit(status)
|
||||
74
core/letsencrypt/jobs/certbot-deploy.py
Executable file
74
core/letsencrypt/jobs/certbot-deploy.py
Executable file
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import sys, os, traceback, tarfile
|
||||
from io import BytesIO
|
||||
|
||||
|
||||
sys.path.append("/opt/bunkerweb/deps/python")
|
||||
sys.path.append("/opt/bunkerweb/utils")
|
||||
sys.path.append("/opt/bunkerweb/api")
|
||||
|
||||
from logger import log
|
||||
from API import API
|
||||
|
||||
status = 0
|
||||
|
||||
try :
|
||||
# Get env vars
|
||||
is_kubernetes_mode = os.getenv("KUBERNETES_MODE") == "yes"
|
||||
is_swarm_mode = os.getenv("SWARM_MODE") == "yes"
|
||||
token = os.getenv("CERTBOT_TOKEN")
|
||||
|
||||
# Cluster case
|
||||
if is_kubernetes_mode or is_swarm_mode :
|
||||
|
||||
# Create tarball of /data/letsencrypt
|
||||
tgz = BytesIO()
|
||||
with tarfile.open(mode="w:gz", fileobj=tgz) as tf :
|
||||
tf.add("/data/letsencrypt", arcname=".")
|
||||
tgz.seek(0, 0)
|
||||
files = {"archive.tar.gz": tgz}
|
||||
|
||||
for variable, value in os.environ.items() :
|
||||
if not variable.startswith("CLUSTER_INSTANCE_") :
|
||||
continue
|
||||
endpoint = value.split(" ")[0]
|
||||
host = value.split(" ")[1]
|
||||
api = API(endpoint, host=host)
|
||||
sent, err, status, resp = api.request("POST", "/lets-encrypt/certificates", files=files)
|
||||
if not sent :
|
||||
status = 1
|
||||
log("LETS-ENCRYPT", "❌", "Can't send API request to " + api.get_endpoint() + "/lets-encrypt/certificates : " + err)
|
||||
else :
|
||||
if status != 200 :
|
||||
status = 1
|
||||
log("LETS-ENCRYPT", "❌", "Error while sending API request to " + api.get_endpoint() + "/lets-encrypt/certificates : status = " + resp["status"] + ", msg = " + resp["msg"])
|
||||
else :
|
||||
log("LETS-ENCRYPT", "ℹ️", "Successfully sent API request to " + api.get_endpoint() + "/lets-encrypt/certificates")
|
||||
sent, err, status, resp = api.request("POST", "/reload")
|
||||
if not sent :
|
||||
status = 1
|
||||
log("LETS-ENCRYPT", "❌", "Can't send API request to " + api.get_endpoint() + "/reload : " + err)
|
||||
else :
|
||||
if status != 200 :
|
||||
status = 1
|
||||
log("LETS-ENCRYPT", "❌", "Error while sending API request to " + api.get_endpoint() + "/reload : status = " + resp["status"] + ", msg = " + resp["msg"])
|
||||
else :
|
||||
log("LETS-ENCRYPT", "ℹ️", "Successfully sent API request to " + api.get_endpoint() + "/reload")
|
||||
|
||||
# Docker or Linux case
|
||||
else :
|
||||
cmd = "/usr/sbin/nginx -s reload"
|
||||
proc = subprocess.run(cmd.split(" "), stdin=subprocess.DEVNULL, stderr=subprocess.STDOUT)
|
||||
if proc.returncode != 0 :
|
||||
status = 1
|
||||
log("LETS-ENCRYPT", "❌", "Error while reloading nginx")
|
||||
else :
|
||||
log("LETS-ENCRYPT", "ℹ️", "Successfully reloaded nginx")
|
||||
|
||||
except :
|
||||
status = 1
|
||||
log("LETS-ENCRYPT", "❌", "Exception while running certbot-deploy.py :")
|
||||
print(traceback.format_exc())
|
||||
|
||||
sys.exit(status)
|
||||
66
core/letsencrypt/jobs/certbot-new.py
Executable file
66
core/letsencrypt/jobs/certbot-new.py
Executable file
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import sys, os, subprocess, traceback
|
||||
|
||||
sys.path.append("/opt/bunkerweb/deps/python")
|
||||
sys.path.append("/opt/bunkerweb/utils")
|
||||
|
||||
import logger
|
||||
|
||||
def certbot_new(first_domain, domains, email) :
|
||||
cmd = "/opt/bunkerweb/deps/python/bin/certbot certonly --manual --preferred-challenges=http --manual-auth-hook /opt/bunkerweb/core/letsencrypt/jobs/certbot-auth.py --manual-cleanup-hook /opt/bunkerweb/core/letsencrypt/jobs/certbot-cleanup.py -n -d " + domains + " --email " + email + " --agree-tos"
|
||||
if os.getenv("USE_LETS_ENCRYPT_STAGING") == "yes" :
|
||||
cmd += " --staging"
|
||||
os.environ["PYTHONPATH"] = "/opt/bunkerweb/deps/python"
|
||||
proc = subprocess.run(cmd.split(" "), stdin=subprocess.DEVNULL, stderr=subprocess.STDOUT, env=os.environ)
|
||||
return proc.returncode
|
||||
|
||||
status = 0
|
||||
|
||||
try :
|
||||
|
||||
# Multisite case
|
||||
if os.getenv("MULTISITE") == "yes" :
|
||||
for first_server in os.getenv("SERVER_NAME").split(" ") :
|
||||
if os.getenv(first_server + "_AUTO_LETS_ENCRYPT", os.getenv("AUTO_LETS_ENCRYPT")) != "yes" :
|
||||
continue
|
||||
if first_server == "" :
|
||||
continue
|
||||
real_server_name = os.getenv(first_server + "_SERVER_NAME", first_server)
|
||||
domains = real_server_name.replace(" ", ",")
|
||||
if os.path.exists("/etc/letsencrypt/live/" + first_server + "/cert.pem") :
|
||||
logger.log("LETS-ENCRYPT", "ℹ️", "Certificates already exists for domain(s) " + domains)
|
||||
continue
|
||||
real_email = os.getenv(first_server + "_EMAIL_LETS_ENCRYPT", os.getenv("EMAIL_LETS_ENCRYPT", "contact@" + first_server))
|
||||
if real_email == "" :
|
||||
real_email = "contact@" + first_server
|
||||
logger.log("LETS-ENCRYPT", "ℹ️", "Asking certificates for domains : " + domains + " (email = " + real_email + ") ...")
|
||||
if certbot_new(first_server, domains, real_email) != 0 :
|
||||
status = 1
|
||||
logger.log("LETS-ENCRYPT", "❌", "Certificate generation failed for domain(s) " + domains + " ...")
|
||||
else :
|
||||
logger.log("LETS-ENCRYPT", "ℹ️", "Certificate generation succeeded for domain(s) : " + domains)
|
||||
|
||||
# Singlesite case
|
||||
elif os.getenv("AUTO_LETS_ENCRYPT") == "yes" and os.getenv("SERVER_NAME") != "" :
|
||||
first_server = os.getenv("SERVER_NAME").split(" ")[0]
|
||||
domains = os.getenv("SERVER_NAME").replace(" ", ",")
|
||||
if not os.path.exists("/etc/letsencrypt/live/" + first_server + "/cert.pem") :
|
||||
logger.log("LETS-ENCRYPT", "ℹ️", "Certificates already exists for domain(s) " + domains)
|
||||
else :
|
||||
real_email = os.getenv("EMAIL_LETS_ENCRYPT", "contact@" + first_server)
|
||||
if real_email == "" :
|
||||
real_email = "contact@" + first_server
|
||||
logger.log("LETS-ENCRYPT", "ℹ️", "Asking certificates for domain(s) : " + domains + " (email = " + real_email + ") ...")
|
||||
if certbot_new(first_server, domains, real_email) != 0 :
|
||||
status = 2
|
||||
logger.log("LETS-ENCRYPT", "❌", "Certificate generation failed for domain(s) : " + domains)
|
||||
else :
|
||||
logger.log("LETS-ENCRYPT", "ℹ️", "Certificate generation succeeded for domain(s) : " + domains)
|
||||
|
||||
except :
|
||||
status = 1
|
||||
logger.log("LETS-ENCRYPT", "❌", "Exception while running certbot-new.py :")
|
||||
print(traceback.format_exc())
|
||||
|
||||
sys.exit(status)
|
||||
50
core/letsencrypt/jobs/certbot-renew.py
Executable file
50
core/letsencrypt/jobs/certbot-renew.py
Executable file
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import sys, os, subprocess, traceback
|
||||
|
||||
sys.path.append("/opt/bunkerweb/deps/python")
|
||||
sys.path.append("/opt/bunkerweb/utils")
|
||||
|
||||
import logger
|
||||
|
||||
def renew(domain) :
|
||||
cmd = "/opt/bunkerweb/deps/python/bin/certbot renew --cert-name " + domain + " --deploy-hook /opt/bunkerweb/core/letsencrypt/jobs/certbot-deploy.py"
|
||||
os.environ["PYTHONPATH"] = "/opt/bunkerweb/deps/python"
|
||||
proc = subprocess.run(cmd.split(" "), stdin=subprocess.DEVNULL, stderr=subprocess.STDOUT, env=os.environ)
|
||||
return proc.returncode
|
||||
|
||||
status = 0
|
||||
|
||||
try :
|
||||
|
||||
if os.getenv("MULTISITE") == "yes" :
|
||||
for first_server in os.getenv("SERVER_NAME").split(" ") :
|
||||
if first_server == "" :
|
||||
continue
|
||||
if os.getenv(first_server + "_AUTO_LETS_ENCRYPT", os.getenv("AUTO_LETS_ENCRYPT")) != "yes" :
|
||||
continue
|
||||
if not os.path.exists("/etc/letsencrypt/live/" + first_server + "/cert.pem") :
|
||||
continue
|
||||
ret = renew(first_server)
|
||||
if ret != 0 :
|
||||
status = 2
|
||||
logger.log("LETS-ENCRYPT", "❌", "Certificates renewal for " + first_server + " failed")
|
||||
else :
|
||||
logger.log("LETS-ENCRYPT", "ℹ️", "Certificates renewal for " + first_server + " successful")
|
||||
|
||||
elif os.getenv("AUTO_LETS_ENCRYPT") == "yes" and os.getenv("SERVER_NAME") != "" :
|
||||
first_server = os.getenv("SERVER_NAME").split(" ")[0]
|
||||
if os.path.exists("/etc/letsencrypt/live/" + first_server + "/cert.pem") :
|
||||
ret = renew(first_server)
|
||||
if ret != 0 :
|
||||
status = 2
|
||||
logger.log("LETS-ENCRYPT", "❌", "Certificates renewal for " + first_server + " failed")
|
||||
else :
|
||||
logger.log("LETS-ENCRYPT", "ℹ️", "Certificates renewal for " + first_server + " successful")
|
||||
|
||||
except :
|
||||
status = 2
|
||||
logger.log("LETS-ENCRYPT", "❌", "Exception while running certbot-renew.py :")
|
||||
print(traceback.format_exc())
|
||||
|
||||
sys.exit(status)
|
||||
49
core/letsencrypt/letsencrypt.lua
Normal file
49
core/letsencrypt/letsencrypt.lua
Normal file
@@ -0,0 +1,49 @@
|
||||
local _M = {}
|
||||
_M.__index = _M
|
||||
|
||||
local logger = require "logger"
|
||||
local cjson = require "cjson"
|
||||
|
||||
function _M.new()
|
||||
local self = setmetatable({}, _M)
|
||||
return self, nil
|
||||
end
|
||||
|
||||
function _M:access()
|
||||
if string.sub(ngx.var.uri, 1, string.len("/.well-known/acme-challenge/")) == "/.well-known/acme-challenge/" then
|
||||
logger.log(ngx.NOTICE, "LETS-ENCRYPT", "Got a visit from Let's Encrypt, let's whitelist it.")
|
||||
return true, "success", true, ngx.exit(ngx.OK)
|
||||
end
|
||||
return true, "success", false, nil
|
||||
end
|
||||
|
||||
function _M:api()
|
||||
if not string.match(ngx.var.uri, "^/lets%-encrypt/challenge$") or (ngx.var.request_method ~= "POST" and ngx.var.request_method ~= "DELETE") then
|
||||
return false, nil, nil
|
||||
end
|
||||
local acme_folder = "/opt/bunkerweb/tmp/lets-encrypt/.well-known/acme-challenge/"
|
||||
ngx.req.read_body()
|
||||
local ret, data = pcall(cjson.decode, ngx.req.get_body_data())
|
||||
if not ret then
|
||||
return true, ngx.HTTP_BAD_REQUEST, {status = "error", msg = "json body decoding failed"}
|
||||
end
|
||||
os.execute("mkdir -p " .. acme_folder)
|
||||
if ngx.var.request_method == "POST" then
|
||||
local file, err = io.open(acme_folder .. data.token, "w+")
|
||||
if not file then
|
||||
return true, ngx.HTTP_INTERNAL_SERVER_ERROR, {status = "error", msg = "can't write validation token : " .. err}
|
||||
end
|
||||
file:write(data.validation)
|
||||
file:close()
|
||||
return true, ngx.HTTP_OK, {status = "success", msg = "validation token written"}
|
||||
elseif ngx.var.request_method == "DELETE" then
|
||||
local ok, err = os.remove(acme_folder .. data.token)
|
||||
if not ok then
|
||||
return true, ngx.HTTP_INTERNAL_SERVER_ERROR, {status = "error", msg = "can't remove validation token : " .. err}
|
||||
end
|
||||
return true, ngx.HTTP_OK, {status = "success", msg = "validation token removed"}
|
||||
end
|
||||
return true, ngx.HTTP_NOT_FOUND, {status = "error", msg = "unknown request"}
|
||||
end
|
||||
|
||||
return _M
|
||||
50
core/letsencrypt/plugin.json
Normal file
50
core/letsencrypt/plugin.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"id": "letsencrypt",
|
||||
"order": 1,
|
||||
"name": "Let's Encrypt",
|
||||
"description": "Automatic creation, renewal and configuration of Let's Encrypt certificates.",
|
||||
"version": "0.1",
|
||||
"settings": {
|
||||
"AUTO_LETS_ENCRYPT": {
|
||||
"context": "multisite",
|
||||
"default": "no",
|
||||
"help": "Activate automatic Let's Encrypt mode.",
|
||||
"id": "auto-lets-encrypt",
|
||||
"label": "Automatic Let's Encrypt",
|
||||
"regex": "^(yes|no)$",
|
||||
"type": "check"
|
||||
},
|
||||
"EMAIL_LETS_ENCRYPT": {
|
||||
"context": "multisite",
|
||||
"default": "",
|
||||
"help": "Email used for Let's Encrypt notification and in certificate.",
|
||||
"id": "email-lets-encrypt",
|
||||
"label": "Email Let's Encrypt",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"USE_LETS_ENCRYPT_STAGING": {
|
||||
"context": "multisite",
|
||||
"default": "no",
|
||||
"help": "Use the staging environment for Let’s Encrypt certificate generation. Useful when you are testing your deployments to avoid being rate limited in the production environment.",
|
||||
"id": "use-lets-encrypt-staging",
|
||||
"label": "Use Let's Encrypt Staging",
|
||||
"regex": "^(yes|no)$",
|
||||
"type": "check"
|
||||
}
|
||||
},
|
||||
"jobs": [
|
||||
{
|
||||
"name": "certbot-new",
|
||||
"file": "certbot-new.py",
|
||||
"every": "once",
|
||||
"reload": false
|
||||
},
|
||||
{
|
||||
"name": "certbot-renew",
|
||||
"file": "certbot-renew.py",
|
||||
"every": "day",
|
||||
"reload": true
|
||||
}
|
||||
]
|
||||
}
|
||||
20
core/limit/confs/http/limitconn.conf
Normal file
20
core/limit/confs/http/limitconn.conf
Normal file
@@ -0,0 +1,20 @@
|
||||
{% if has_variable(all, "USE_LIMIT_CONN", "yes") +%}
|
||||
|
||||
map $http2 $v1ip {
|
||||
default "";
|
||||
"" $binary_remote_addr;
|
||||
}
|
||||
|
||||
map $http2 $v2ip {
|
||||
default $binary_remote_addr;
|
||||
"" "";
|
||||
}
|
||||
|
||||
limit_conn_zone $v1ip zone=v1ips:10m;
|
||||
limit_conn_zone $v2ip zone=v2ips:10m;
|
||||
|
||||
limit_conn_log_level warn;
|
||||
|
||||
limit_conn_status 429;
|
||||
|
||||
{% endif %}
|
||||
6
core/limit/confs/server-http/limitconn.conf
Normal file
6
core/limit/confs/server-http/limitconn.conf
Normal file
@@ -0,0 +1,6 @@
|
||||
{% if USE_LIMIT_CONN == "yes" +%}
|
||||
|
||||
limit_conn v1ips {{ LIMIT_CONN_MAX_HTTP1 }};
|
||||
limit_conn v2ips {{ LIMIT_CONN_MAX_HTTP2 }};
|
||||
|
||||
{% endif %}
|
||||
150
core/limit/limit.lua
Normal file
150
core/limit/limit.lua
Normal file
@@ -0,0 +1,150 @@
|
||||
local _M = {}
|
||||
_M.__index = _M
|
||||
|
||||
local utils = require "utils"
|
||||
local datastore = require "datastore"
|
||||
local logger = require "logger"
|
||||
local cjson = require "cjson"
|
||||
|
||||
function _M.new()
|
||||
local self = setmetatable({}, _M)
|
||||
return self, nil
|
||||
end
|
||||
|
||||
function _M:init()
|
||||
-- Check if init is needed
|
||||
local init_needed, err = utils.has_variable("USE_LIMIT_REQ", "yes")
|
||||
if init_needed == nil then
|
||||
return false, err
|
||||
end
|
||||
if not init_needed then
|
||||
return true, "no service uses Limit for requests, skipping init"
|
||||
end
|
||||
-- Get variables
|
||||
local variables, err = utils.get_multiple_variables({"LIMIT_REQ_URL", "LIMIT_REQ_RATE"})
|
||||
if variables == nil then
|
||||
return false, err
|
||||
end
|
||||
-- Store URLs and rates
|
||||
local data = {}
|
||||
local i = 0
|
||||
for srv, vars in pairs(variables) do
|
||||
for var, value in pairs(vars) do
|
||||
if var:match("LIMIT_REQ_URL") then
|
||||
local url = value
|
||||
local rate = vars[var:gsub("URL", "RATE")]
|
||||
if data[srv] == nil then
|
||||
data[srv] = {}
|
||||
end
|
||||
data[srv][url] = rate
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
local ok, err = datastore:set("plugin_limit_rules", cjson.encode(data))
|
||||
if not ok then
|
||||
return false, err
|
||||
end
|
||||
return true, "successfully loaded " .. tostring(i) .. " limit rules for requests"
|
||||
end
|
||||
|
||||
function _M:access()
|
||||
-- Check if access is needed
|
||||
local access_needed, err = utils.get_variable("USE_LIMIT_REQ")
|
||||
if access_needed == nil then
|
||||
return false, err, nil, nil
|
||||
end
|
||||
if access_needed ~= "yes" then
|
||||
return true, "Limit for request not activated", nil, nil
|
||||
end
|
||||
|
||||
-- Don't go further if URL is not limited
|
||||
local limited = false
|
||||
local all_rules, err = datastore:get("plugin_limit_rules")
|
||||
if not all_rules then
|
||||
return false, err, nil, nil
|
||||
end
|
||||
all_rules = cjson.decode(all_rules)
|
||||
local limited = false
|
||||
local rate = ""
|
||||
if not limited and all_rules[ngx.var.server_name] then
|
||||
for k, v in pairs(all_rules[ngx.var.server_name]) do
|
||||
if ngx.var.uri:match(k) and k ~= "/" then
|
||||
limited = true
|
||||
rate = all_rules[ngx.var.server_name][k]
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if all_rules.global and not limited then
|
||||
for k, v in pairs(all_rules.global) do
|
||||
if ngx.var.uri:match(k) and k ~= "/" then
|
||||
limited = true
|
||||
rate = all_rules.global[k]
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if not limited then
|
||||
if all_rules[ngx.var.server_name] and all_rules[ngx.var.server_name]["/"] then
|
||||
limited = true
|
||||
rate = all_rules[ngx.var.server_name]["/"]
|
||||
elseif all_rules.global and all_rules.global["/"] then
|
||||
limited = true
|
||||
rate = all_rules.global["/"]
|
||||
end
|
||||
if not limited then
|
||||
return true, "URL " .. ngx.var.uri .. " is not limited by a rule, skipping check", nil, nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Get the rate
|
||||
local _, _, rate_max, rate_time = rate:find("(%d+)r/(.)")
|
||||
|
||||
-- Get current requests timestamps
|
||||
local requests, err = datastore:get("plugin_limit_cache_" .. ngx.var.remote_addr .. ngx.var.uri)
|
||||
if not requests and err ~= "not found" then
|
||||
return false, err, nil, nil
|
||||
elseif err == "not found" then
|
||||
requests = "{}"
|
||||
end
|
||||
|
||||
-- Compute new timestamps
|
||||
local new_timestamps = {}
|
||||
local current_timestamp = os.time(os.date("!*t"))
|
||||
local delay = 0
|
||||
if rate_time == "s" then
|
||||
delay = 1
|
||||
elseif rate_time == "m" then
|
||||
delay = 60
|
||||
elseif rate_time == "h" then
|
||||
delay = 3600
|
||||
elseif rate_time == "d" then
|
||||
delay = 86400
|
||||
end
|
||||
for i, timestamp in ipairs(cjson.decode(requests)) do
|
||||
if current_timestamp - timestamp <= delay then
|
||||
table.insert(new_timestamps, timestamp)
|
||||
end
|
||||
end
|
||||
-- Only insert the new timestamp if client is not limited already to avoid infinite insert
|
||||
if #new_timestamps <= tonumber(rate_max) then
|
||||
table.insert(new_timestamps, current_timestamp)
|
||||
end
|
||||
|
||||
-- Save the new timestamps
|
||||
local ok, err = datastore:set("plugin_limit_cache_" .. ngx.var.remote_addr .. ngx.var.uri, cjson.encode(new_timestamps), delay)
|
||||
if not ok then
|
||||
return false, "can't update timestamps : " .. err, nil, nil
|
||||
end
|
||||
|
||||
-- Deny if the rate is higher than the one defined in rule
|
||||
if #new_timestamps > tonumber(rate_max) then
|
||||
return true, "client IP " .. ngx.var.remote_addr .. " is limited for URL " .. ngx.var.uri .. " (current rate = " .. tostring(#new_timestamps) .. "r/" .. rate_time .. " and max rate = " .. rate .. ")", true, ngx.HTTP_TOO_MANY_REQUESTS
|
||||
end
|
||||
|
||||
-- Limit not reached
|
||||
return true, "client IP " .. ngx.var.remote_addr .. " is not limited for URL " .. ngx.var.uri .. " (current rate = " .. tostring(#new_timestamps) .. "r/" .. rate_time .. " and max rate = " .. rate .. ")", nil, nil
|
||||
end
|
||||
|
||||
return _M
|
||||
65
core/limit/plugin.json
Normal file
65
core/limit/plugin.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"id": "limit",
|
||||
"order": 3,
|
||||
"name": "Limit",
|
||||
"description": "Limit maximum number of requests and connections.",
|
||||
"version": "0.1",
|
||||
"settings": {
|
||||
"USE_LIMIT_REQ": {
|
||||
"context": "multisite",
|
||||
"default": "yes",
|
||||
"help": "Activate limit requests feature.",
|
||||
"id": "use-limit-req",
|
||||
"label": "Activate limit requests",
|
||||
"regex": "^(yes|no)$",
|
||||
"type": "check"
|
||||
},
|
||||
"LIMIT_REQ_URL": {
|
||||
"context": "multisite",
|
||||
"default": "/",
|
||||
"help": "URL where the limit request will be applied.",
|
||||
"id": "limit-req-url",
|
||||
"label": "URL",
|
||||
"regex": "^.*$",
|
||||
"type": "text",
|
||||
"multiple": "limit-req"
|
||||
},
|
||||
"LIMIT_REQ_RATE": {
|
||||
"context": "multisite",
|
||||
"default": "2r/s",
|
||||
"help": "Rate to apply to the URL (s for second, m for minute, h for hour and d for day).",
|
||||
"id": "limit-req-rate",
|
||||
"label": "Rate",
|
||||
"regex": "^.*$",
|
||||
"type": "text",
|
||||
"multiple": "limit-req"
|
||||
},
|
||||
"USE_LIMIT_CONN": {
|
||||
"context": "multisite",
|
||||
"default": "yes",
|
||||
"help": "Activate limit connections feature.",
|
||||
"id": "use-limit-conn",
|
||||
"label": "Activate limit connections",
|
||||
"regex": "^(yes|no)$",
|
||||
"type": "check"
|
||||
},
|
||||
"LIMIT_CONN_MAX_HTTP1": {
|
||||
"context": "multisite",
|
||||
"default": "10",
|
||||
"help": "Maximum number of connections per IP when using HTTP/1.X protocol.",
|
||||
"id": "limit-conn-max-http1",
|
||||
"label": "Maximum number of HTTP/1.X connections",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"LIMIT_CONN_MAX_HTTP2": {
|
||||
"context": "multisite",
|
||||
"default": "100",
|
||||
"help": "Maximum number of streams per IP when using HTTP/2 protocol.",
|
||||
"id": "limit-conn-max-http2",
|
||||
"label": "Maximum number of HTTP/2 streams",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
5
core/misc/confs/default-server-http/disable.conf
Normal file
5
core/misc/confs/default-server-http/disable.conf
Normal file
@@ -0,0 +1,5 @@
|
||||
{% if DISABLE_DEFAULT_SERVER == "yes" +%}
|
||||
location / {
|
||||
return 444;
|
||||
}
|
||||
{% endif %}
|
||||
5
core/misc/confs/server-http/allowed-methods.conf
Normal file
5
core/misc/confs/server-http/allowed-methods.conf
Normal file
@@ -0,0 +1,5 @@
|
||||
{% if ALLOWED_METHODS != "" +%}
|
||||
if ($request_method !~ ^({{ ALLOWED_METHODS }})$) {
|
||||
return 405;
|
||||
}
|
||||
{% endif %}
|
||||
1
core/misc/confs/server-http/max-client-size.conf
Normal file
1
core/misc/confs/server-http/max-client-size.conf
Normal file
@@ -0,0 +1 @@
|
||||
client_max_body_size {{ MAX_CLIENT_SIZE }};
|
||||
4
core/misc/confs/server-http/open-file-cache.conf
Normal file
4
core/misc/confs/server-http/open-file-cache.conf
Normal file
@@ -0,0 +1,4 @@
|
||||
open_file_cache {{ OPEN_FILE_CACHE }};
|
||||
open_file_cache_errors {% if OPEN_FILE_CACHE_ERRORS == "yes" +%} on {% else +%} off {% endif +%};
|
||||
open_file_cache_min_uses {{ OPEN_FILE_CACHE_MIN_USES }};
|
||||
open_file_cache_valid {{ OPEN_FILE_CACHE_VALID }};
|
||||
12
core/misc/confs/server-http/redirect-http-to-https.conf
Normal file
12
core/misc/confs/server-http/redirect-http-to-https.conf
Normal file
@@ -0,0 +1,12 @@
|
||||
{% if REDIRECT_HTTP_TO_HTTPS == "yes" +%}
|
||||
if ($scheme = http) {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
{% elif AUTO_REDIRECT_HTTP_TO_HTTPS == "yes" +%}
|
||||
{% if AUTO_LETS_ENCRYPT == "yes" or USE_CUSTOM_HTTPS == "yes" or GENERATE_SELF_SIGNED_SSL == "yes" +%}
|
||||
if ($scheme = http) {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
{% endif +%}
|
||||
{% endif +%}
|
||||
|
||||
6
core/misc/confs/server-http/serve-files.conf
Normal file
6
core/misc/confs/server-http/serve-files.conf
Normal file
@@ -0,0 +1,6 @@
|
||||
{% if SERVE_FILES == "yes" +%}
|
||||
root {% if ROOT_FOLDER == "" %}/opt/bunkerweb/www/{% if MULTISITE == "yes" %}{{ SERVER_NAME.split(" ")[0] }}{% endif %}{% else %}{{ ROOT_FOLDER }}{% endif %};
|
||||
try_files $uri $uri/ =404;
|
||||
{% else +%}
|
||||
root /nowhere;
|
||||
{% endif %}
|
||||
144
core/misc/plugin.json
Normal file
144
core/misc/plugin.json
Normal file
@@ -0,0 +1,144 @@
|
||||
{
|
||||
"id": "misc",
|
||||
"order": 999,
|
||||
"name": "Miscellaneous",
|
||||
"description": "Miscellaneous settings.",
|
||||
"version": "0.1",
|
||||
"settings": {
|
||||
"DISABLE_DEFAULT_SERVER": {
|
||||
"context": "global",
|
||||
"default": "no",
|
||||
"help": "Close connection if the request vhost is unknown.",
|
||||
"id": "disable-default-server",
|
||||
"label": "Disable default server",
|
||||
"regex": "^(yes|no)$",
|
||||
"type": "check"
|
||||
},
|
||||
"REDIRECT_HTTP_TO_HTTPS": {
|
||||
"context": "multisite",
|
||||
"default": "no",
|
||||
"help": "Redirect all HTTP request to HTTPS.",
|
||||
"id": "redirect-http-to-https",
|
||||
"label": "Redirect HTTP to HTTPS",
|
||||
"regex": ".*",
|
||||
"type": "text"
|
||||
},
|
||||
"AUTO_REDIRECT_HTTP_TO_HTTPS": {
|
||||
"context": "multisite",
|
||||
"default": "yes",
|
||||
"help": "Try to detect if HTTPS is used and activate HTTP to HTTPS redirection if that's the case.",
|
||||
"id": "auto-redirect-http-to-https",
|
||||
"label": "Auto redirect HTTP to HTTPS",
|
||||
"regex": ".*",
|
||||
"type": "text"
|
||||
},
|
||||
"ALLOWED_METHODS": {
|
||||
"context": "multisite",
|
||||
"default": "GET|POST|HEAD",
|
||||
"help": "Allowed HTTP methods to be sent by clients.",
|
||||
"id": "allowed-methods",
|
||||
"label": "Allowed methods",
|
||||
"regex": ".*",
|
||||
"type": "text"
|
||||
},
|
||||
"MAX_CLIENT_SIZE": {
|
||||
"context": "multisite",
|
||||
"default": "10m",
|
||||
"help": "Maximum body size (0 for infinite).",
|
||||
"id": "max-client-size",
|
||||
"label": "Maximum body size",
|
||||
"regex": ".*",
|
||||
"type": "text"
|
||||
},
|
||||
"SERVE_FILES": {
|
||||
"context": "multisite",
|
||||
"default": "yes",
|
||||
"help": "Serve files from the local folder.",
|
||||
"id": "serve-files",
|
||||
"label": "Serve files",
|
||||
"regex": "^(yes|no)$",
|
||||
"type": "check"
|
||||
},
|
||||
"ROOT_FOLDER": {
|
||||
"context": "multisite",
|
||||
"default": "",
|
||||
"help": "Root folder containing files to serve (/opt/bunkerweb/www/{server_name} if unset).",
|
||||
"id": "root-folder",
|
||||
"label": "Root folder",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"HTTPS_PROTOCOLS": {
|
||||
"context": "multisite",
|
||||
"default": "TLSv1.2 TLSv1.3",
|
||||
"help": "The supported version of TLS. We recommend the default value TLSv1.2 TLSv1.3 for compatibility reasons.",
|
||||
"id": "https-protocols",
|
||||
"label": "HTTPS protocols",
|
||||
"regex": ".*",
|
||||
"type": "text"
|
||||
},
|
||||
"HTTP2": {
|
||||
"context": "multisite",
|
||||
"default": "yes",
|
||||
"help": "Support HTTP2 protocol when HTTPS is enabled.",
|
||||
"id": "http2",
|
||||
"label": "HTTP2",
|
||||
"regex": ".*",
|
||||
"type": "check"
|
||||
},
|
||||
"LISTEN_HTTP": {
|
||||
"context": "multisite",
|
||||
"default": "yes",
|
||||
"help": "Respond to (insecure) HTTP requests.",
|
||||
"id": "http-listen",
|
||||
"label": "HTTP listen",
|
||||
"regex": "^(yes|no)$",
|
||||
"type": "check"
|
||||
},
|
||||
"USE_OPEN_FILE_CACHE" : {
|
||||
"context": "multisite",
|
||||
"default": "no",
|
||||
"help": "Enable open file cache feature",
|
||||
"id": "use-open-file-cache",
|
||||
"label": "Use open file cache",
|
||||
"regex": "^(yes|no)$",
|
||||
"type": "check"
|
||||
},
|
||||
"OPEN_FILE_CACHE" : {
|
||||
"context": "multisite",
|
||||
"default": "max=1000 inactive=20s",
|
||||
"help": "Open file cache directive",
|
||||
"id": "open-file-cache",
|
||||
"label": "Use open file cache",
|
||||
"regex": "^.*$",
|
||||
"type": "text"
|
||||
},
|
||||
"OPEN_FILE_CACHE_ERRORS" : {
|
||||
"context": "multisite",
|
||||
"default": "yes",
|
||||
"help": "Enable open file cache for errors",
|
||||
"id": "open-file-cache-errors",
|
||||
"label": "Open file cache errors",
|
||||
"regex": "^(yes|no)$",
|
||||
"type": "text"
|
||||
},
|
||||
"OPEN_FILE_CACHE_MIN_USES" : {
|
||||
"context": "multisite",
|
||||
"default": "2",
|
||||
"help": "Enable open file cache minimum uses",
|
||||
"id": "open-file-cache-min-uses",
|
||||
"label": "Open file cache min uses",
|
||||
"regex": "^([1-9]+)$",
|
||||
"type": "text"
|
||||
},
|
||||
"OPEN_FILE_CACHE_VALID" : {
|
||||
"context": "multisite",
|
||||
"default": "30s",
|
||||
"help": "Open file cache valid time",
|
||||
"id": "open-file-cache-valid",
|
||||
"label": "Open file cache valid time",
|
||||
"regex": "^\\d+(ms|s|m|h|d|w|M|y)$",
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
110
core/modsecurity/confs/server-http/modsecurity-rules.conf.modsec
Normal file
110
core/modsecurity/confs/server-http/modsecurity-rules.conf.modsec
Normal file
@@ -0,0 +1,110 @@
|
||||
# process rules with disruptive actions
|
||||
SecRuleEngine On
|
||||
|
||||
# allow body checks
|
||||
SecRequestBodyAccess On
|
||||
|
||||
# enable XML parsing
|
||||
SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\+|/)|text/)xml" \
|
||||
"id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML"
|
||||
|
||||
# enable JSON parsing
|
||||
SecRule REQUEST_HEADERS:Content-Type "application/json" \
|
||||
"id:'200001',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON"
|
||||
|
||||
# maximum data size
|
||||
SecRequestBodyLimit 13107200
|
||||
SecRequestBodyNoFilesLimit 131072
|
||||
|
||||
# reject requests if bigger than max data size
|
||||
SecRequestBodyLimitAction Reject
|
||||
|
||||
# reject if we can't process the body
|
||||
SecRule REQBODY_ERROR "!@eq 0" \
|
||||
"id:'200002', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2"
|
||||
|
||||
# be strict with multipart/form-data body
|
||||
SecRule MULTIPART_STRICT_ERROR "!@eq 0" \
|
||||
"id:'200003',phase:2,t:none,log,deny,status:400, \
|
||||
msg:'Multipart request body failed strict validation: \
|
||||
PE %{REQBODY_PROCESSOR_ERROR}, \
|
||||
BQ %{MULTIPART_BOUNDARY_QUOTED}, \
|
||||
BW %{MULTIPART_BOUNDARY_WHITESPACE}, \
|
||||
DB %{MULTIPART_DATA_BEFORE}, \
|
||||
DA %{MULTIPART_DATA_AFTER}, \
|
||||
HF %{MULTIPART_HEADER_FOLDING}, \
|
||||
LF %{MULTIPART_LF_LINE}, \
|
||||
SM %{MULTIPART_MISSING_SEMICOLON}, \
|
||||
IQ %{MULTIPART_INVALID_QUOTING}, \
|
||||
IP %{MULTIPART_INVALID_PART}, \
|
||||
IH %{MULTIPART_INVALID_HEADER_FOLDING}, \
|
||||
FL %{MULTIPART_FILE_LIMIT_EXCEEDED}'"
|
||||
SecRule MULTIPART_UNMATCHED_BOUNDARY "@eq 1" \
|
||||
"id:'200004',phase:2,t:none,log,deny,msg:'Multipart parser detected a possible unmatched boundary.'"
|
||||
|
||||
# enable response body checks
|
||||
SecResponseBodyAccess On
|
||||
SecResponseBodyMimeType text/plain text/html text/xml application/json
|
||||
SecResponseBodyLimit 524288
|
||||
SecResponseBodyLimitAction ProcessPartial
|
||||
|
||||
# log usefull stuff
|
||||
SecAuditEngine {{ MODSECURITY_SEC_AUDIT_ENGINE }}
|
||||
SecAuditLogType Serial
|
||||
SecAuditLog /var/log/nginx/modsec_audit.log
|
||||
|
||||
# include OWASP CRS configurations
|
||||
{% if USE_MODSECURITY_CRS == "yes" %}
|
||||
include /opt/bunkerweb/core/modsecurity/files/crs-setup.conf
|
||||
|
||||
# custom CRS configurations before loading rules (e.g. exclusions)
|
||||
{% if is_custom_conf("/opt/bunkerweb/configs/modsec-crs") %}
|
||||
include /opt/bunkerweb/configs/modsec-crs/*.conf
|
||||
{% endif %}
|
||||
{% if MULTISITE == "yes" and is_custom_conf("/opt/bunkerweb/configs/modsec-crs/" + SERVER_NAME.split(" ")[0]) %}
|
||||
include /opt/bunkerweb/configs/modsec-crs/{{ SERVER_NAME.split(" ")[0] }}/*.conf
|
||||
{% endif %}
|
||||
{% if is_custom_conf("/etc/nginx/modsec-crs") %}
|
||||
include /etc/nginx/modsec-crs/*.conf
|
||||
{% endif %}
|
||||
{% if MULTISITE == "yes" and is_custom_conf("/etc/nginx/" + SERVER_NAME.split(" ")[0] + "/modsec-crs/") %}
|
||||
include /etc/nginx/{{ SERVER_NAME.split(" ")[0] }}/modsec-crs/*.conf
|
||||
{% endif %}
|
||||
|
||||
# unset REASON env var
|
||||
SecAction "nolog,phase:1,setenv:REASON=none"
|
||||
|
||||
# Auto update allowed methods
|
||||
{% if ALLOWED_METHODS != "" +%}
|
||||
SecAction \
|
||||
"id:900200,\
|
||||
phase:1,\
|
||||
nolog,\
|
||||
pass,\
|
||||
t:none,\
|
||||
setvar:'tx.allowed_methods={{ ALLOWED_METHODS.replace("|", " ") }}'"
|
||||
|
||||
{% endif +%}
|
||||
# include OWASP CRS rules
|
||||
include /opt/bunkerweb/core/modsecurity/files/coreruleset/rules/*.conf
|
||||
{% endif %}
|
||||
|
||||
# custom rules after loading the CRS
|
||||
{% if is_custom_conf("/opt/bunkerweb/configs/modsec") %}
|
||||
include /opt/bunkerweb/configs/modsec/*.conf
|
||||
{% endif %}
|
||||
{% if MULTISITE == "yes" and is_custom_conf("/opt/bunkerweb/configs/modsec/" + SERVER_NAME.split(" ")[0]) %}
|
||||
include /opt/bunkerweb/configs/modsec/{{ SERVER_NAME.split(" ")[0] }}/*.conf
|
||||
{% endif %}
|
||||
{% if is_custom_conf("/etc/nginx/modsec") %}
|
||||
include /etc/nginx/modsec/*.conf
|
||||
{% endif %}
|
||||
{% if MULTISITE == "yes" and is_custom_conf("/etc/nginx/" + SERVER_NAME.split(" ")[0] + "/modsec") %}
|
||||
include /etc/nginx/{{ SERVER_NAME.split(" ")[0] }}/modsec/*.conf
|
||||
{% endif %}
|
||||
|
||||
# set REASON env var
|
||||
{% if USE_MODSECURITY_CRS == "yes" %}
|
||||
SecRuleUpdateActionById 949110 "t:none,deny,status:403,setenv:REASON=modsecurity"
|
||||
SecRuleUpdateActionById 959100 "t:none,deny,status:403,setenv:REASON=modsecurity"
|
||||
{% endif %}
|
||||
4
core/modsecurity/confs/server-http/modsecurity.conf
Normal file
4
core/modsecurity/confs/server-http/modsecurity.conf
Normal file
@@ -0,0 +1,4 @@
|
||||
{% if USE_MODSECURITY == "yes" +%}
|
||||
modsecurity on;
|
||||
modsecurity_rules_file {{ NGINX_PREFIX }}server-http/modsecurity-rules.conf.modsec;
|
||||
{% endif %}
|
||||
34
core/modsecurity/files/coreruleset/.github/ISSUE_TEMPLATE/01_false-positive.md
vendored
Normal file
34
core/modsecurity/files/coreruleset/.github/ISSUE_TEMPLATE/01_false-positive.md
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
name: 'False positive'
|
||||
about: Report a false positive (incorrect blocking)
|
||||
title: ''
|
||||
labels: 'False Positive'
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
### Description
|
||||
|
||||
<!-- Please provide a copy of the audit log entry. You can usually -->
|
||||
<!-- find this at /var/log/modsec_audit.log. -->
|
||||
<!-- Include any relevant CVEs or research links. -->
|
||||
|
||||
### Audit Logs / Triggered Rule Numbers
|
||||
|
||||
<!-- Everything you can provide about a blocked request/response -->
|
||||
<!-- or, at least, a list of triggered CRS rule numbers. -->
|
||||
|
||||
### Your Environment
|
||||
|
||||
<!-- Include as many relevant details about the environment you -->
|
||||
<!-- experienced the bug in: -->
|
||||
|
||||
* CRS version (e.g., v3.2.0):
|
||||
* Paranoia level setting:
|
||||
* ModSecurity version (e.g., 2.9.3):
|
||||
* Web Server and version (e.g., apache 2.4.41):
|
||||
* Operating System and version:
|
||||
|
||||
### Confirmation
|
||||
|
||||
[ ] I have removed any personal data (email addresses, IP addresses,
|
||||
passwords, domain names) from any logs posted.
|
||||
33
core/modsecurity/files/coreruleset/.github/ISSUE_TEMPLATE/02_false-negative.md
vendored
Normal file
33
core/modsecurity/files/coreruleset/.github/ISSUE_TEMPLATE/02_false-negative.md
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: 'False negative'
|
||||
about: Report a false negative (incorrect bypass)
|
||||
title: ''
|
||||
labels: 'False Negative - Evasion'
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
### Description
|
||||
|
||||
<!-- Please provide the payload you are sending. For complex payloads -->
|
||||
<!-- with headers, please include a curl command. -->
|
||||
<!-- Include any relevant CVEs or research links. -->
|
||||
<!-- If the bypass works in paranoia level 4, please consider sending -->
|
||||
<!-- us an email instead. See -->
|
||||
<!-- https://github.com/SpiderLabs/owasp-modsecurity-crs/security/policy -->
|
||||
<!-- for details. -->
|
||||
|
||||
### Your Environment
|
||||
|
||||
<!-- Include as many relevant details about the environment you -->
|
||||
<!-- experienced the bug in: -->
|
||||
|
||||
* CRS version (e.g., v3.2.0):
|
||||
* Paranoia level setting:
|
||||
* ModSecurity version (e.g., 2.9.3):
|
||||
* Web Server and version (e.g., apache 2.4.41):
|
||||
* Operating System and version:
|
||||
|
||||
### Confirmation
|
||||
|
||||
[ ] I have removed any personal data (email addresses, IP addresses,
|
||||
passwords, domain names) from any logs posted.
|
||||
39
core/modsecurity/files/coreruleset/.github/ISSUE_TEMPLATE/03_bug-report.md
vendored
Normal file
39
core/modsecurity/files/coreruleset/.github/ISSUE_TEMPLATE/03_bug-report.md
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: '🐞 Bug report'
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: 'Bug'
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
### Describe the bug
|
||||
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
### Steps to reproduce
|
||||
|
||||
<!-- Include steps that will help us recreate the issue. -->
|
||||
|
||||
### Expected behaviour
|
||||
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
### Actual behaviour
|
||||
|
||||
<!-- A clear and concise description of what actually happened. -->
|
||||
<!-- If applicable, add screenshots to help explain your problem. -->
|
||||
|
||||
### Additional context
|
||||
|
||||
<!-- Add any other context about the problem here. -->
|
||||
|
||||
### Your Environment
|
||||
|
||||
<!-- Include as many relevant details about the environment you -->
|
||||
<!-- experienced the bug in: -->
|
||||
|
||||
* CRS version (e.g., v3.2.0):
|
||||
* Paranoia level setting:
|
||||
* ModSecurity version (e.g., 2.9.3):
|
||||
* Web Server and version (e.g., apache 2.4.41):
|
||||
* Operating System and version:
|
||||
28
core/modsecurity/files/coreruleset/.github/ISSUE_TEMPLATE/04_feature.md
vendored
Normal file
28
core/modsecurity/files/coreruleset/.github/ISSUE_TEMPLATE/04_feature.md
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
name: '🚀 Feature request'
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: 'Feature Request'
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
### Motivation
|
||||
|
||||
<!-- A clear and concise description of what the motivation for the -->
|
||||
<!-- new feature is, and what problem it is solving. -->
|
||||
|
||||
### Proposed solution
|
||||
|
||||
<!-- A clear and concise description of the feature you would like -->
|
||||
<!-- to add, and how it solves the motivating problem. -->
|
||||
|
||||
### Alternatives
|
||||
|
||||
<!-- A clear and concise description of any alternative solutions -->
|
||||
<!-- or features you've considered, and why you're proposed solution is -->
|
||||
<!-- better. -->
|
||||
|
||||
### Additional context
|
||||
|
||||
<!-- Add any other context or screenshots about the feature request -->
|
||||
<!-- here. -->
|
||||
8
core/modsecurity/files/coreruleset/.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
core/modsecurity/files/coreruleset/.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Help and support
|
||||
url: https://security.stackexchange.com/questions/tagged/owasp-crs
|
||||
about: For help and support please go here.
|
||||
- name: OWASP Core Rule Set mailing list
|
||||
url: https://groups.google.com/a/owasp.org/forum/#!forum/modsecurity-core-rule-set-project
|
||||
about: Ask general usage questions and participate in discussions on the CRS.
|
||||
38
core/modsecurity/files/coreruleset/.github/workflows/lint.yaml
vendored
Normal file
38
core/modsecurity/files/coreruleset/.github/workflows/lint.yaml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: Lint
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
check-syntax:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: true
|
||||
# check why is failing and change afterwards
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Lint Yaml
|
||||
uses: ibiqlik/action-yamllint@v1
|
||||
with:
|
||||
file_or_dir: tests/regression/tests/**/*.yaml
|
||||
config_file: .yamllint.yml
|
||||
|
||||
- name: Linelint
|
||||
uses: fernandrone/linelint@master
|
||||
id: linelint
|
||||
|
||||
- name: Set up Python 3
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.6
|
||||
|
||||
- name: "Check CRS syntax"
|
||||
run: |
|
||||
python -V
|
||||
pip install --upgrade setuptools
|
||||
pip install -r tests/integration/requirements.txt
|
||||
git clone https://github.com/CRS-support/secrules_parsing
|
||||
pip install -r secrules_parsing/requirements.txt
|
||||
python secrules_parsing/secrules_parser.py -c -f rules/*.conf
|
||||
19
core/modsecurity/files/coreruleset/.github/workflows/stale.yml
vendored
Normal file
19
core/modsecurity/files/coreruleset/.github/workflows/stale.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: Mark stale issues
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/stale@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'This issue has been open 120 days with no activity. Remove the stale label or comment, or this will be closed in 14 days'
|
||||
stale-issue-label: 'Stale issue'
|
||||
days-before-stale: 120
|
||||
days-before-close: 14
|
||||
74
core/modsecurity/files/coreruleset/.github/workflows/test.yml
vendored
Normal file
74
core/modsecurity/files/coreruleset/.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
---
|
||||
name: Regression Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'rules/**'
|
||||
- 'tests/**'
|
||||
- '.github/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'rules/**'
|
||||
- 'tests/**'
|
||||
- '.github/**'
|
||||
|
||||
jobs:
|
||||
# "modsec2-apache", "modsec3-apache", "modsec3-nginx"
|
||||
regression:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
# change to true
|
||||
fail-fast: false
|
||||
matrix:
|
||||
modsec_version: [modsec2-apache]
|
||||
tests: [REQUEST-911-METHOD-ENFORCEMENT,
|
||||
REQUEST-913-SCANNER-DETECTION,
|
||||
REQUEST-920-PROTOCOL-ENFORCEMENT,
|
||||
REQUEST-921-PROTOCOL-ATTACK,
|
||||
REQUEST-930-APPLICATION-ATTACK-LFI,
|
||||
REQUEST-931-APPLICATION-ATTACK-RFI,
|
||||
REQUEST-932-APPLICATION-ATTACK-RCE,
|
||||
REQUEST-933-APPLICATION-ATTACK-PHP,
|
||||
REQUEST-934-APPLICATION-ATTACK-NODEJS,
|
||||
REQUEST-941-APPLICATION-ATTACK-XSS,
|
||||
REQUEST-942-APPLICATION-ATTACK-SQLI,
|
||||
REQUEST-943-APPLICATION-ATTACK-SESSION-FIXATION,
|
||||
REQUEST-944-APPLICATION-ATTACK-JAVA]
|
||||
# Will include soon for modsec3-nginx
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python 2
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 2.7
|
||||
|
||||
- name: "Run tests for ${{ matrix.modsec_version }}`"
|
||||
env:
|
||||
CONFIG: ${{ matrix.modsec_version }}
|
||||
run: |
|
||||
python -V
|
||||
mkdir -p logs/"${CONFIG}"
|
||||
docker-compose -f ./tests/docker-compose.yml up -d "${CONFIG}"
|
||||
pip install --upgrade setuptools
|
||||
pip install -r tests/regression/requirements.txt
|
||||
# Use mounted volume path
|
||||
if [[ "${CONFIG}" == *"nginx" ]]; then
|
||||
LOGDIR="/var/log/nginx"
|
||||
else
|
||||
LOGDIR="/var/log/apache2"
|
||||
fi
|
||||
sed -ie "s:${LOGDIR}:${GITHUB_WORKSPACE}/logs/${CONFIG}:g" tests/regression/config.ini
|
||||
py.test -vs tests/regression/CRS_Tests.py \
|
||||
--config="${CONFIG}" \
|
||||
--ruledir=./tests/regression/tests/${{ matrix.tests }}
|
||||
|
||||
- name: Clean docker-compose
|
||||
env:
|
||||
CONFIG: modsec2-apache
|
||||
run: |
|
||||
docker-compose -f ./tests/docker-compose.yml stop "${CONFIG}"
|
||||
docker-compose -f ./tests/docker-compose.yml down
|
||||
21
core/modsecurity/files/coreruleset/.gitignore
vendored
Normal file
21
core/modsecurity/files/coreruleset/.gitignore
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# User configuration
|
||||
crs-setup.conf
|
||||
rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf
|
||||
rules/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf
|
||||
|
||||
# The MaxMind GeoIP database can be downloaded or upgraded by running:
|
||||
# util/upgrade.py geoip
|
||||
util/geo-location/GeoIP.dat
|
||||
|
||||
# Unit test caches
|
||||
.cache
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
.idea/
|
||||
4
core/modsecurity/files/coreruleset/.gitmodules
vendored
Normal file
4
core/modsecurity/files/coreruleset/.gitmodules
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
[submodule "docs/OWASP-CRS-Documentation"]
|
||||
path = docs/OWASP-CRS-Documentation
|
||||
url = https://github.com/SpiderLabs/OWASP-CRS-Documentation
|
||||
branch = master
|
||||
12
core/modsecurity/files/coreruleset/.linelint.yml
Normal file
12
core/modsecurity/files/coreruleset/.linelint.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
rules:
|
||||
# checks if file ends in a newline character
|
||||
end-of-file:
|
||||
# set to true to enable this rule
|
||||
enable: true
|
||||
|
||||
# set to true to disable autofix (if enabled globally)
|
||||
disable-autofix: true
|
||||
|
||||
# will be ignored only by this rule
|
||||
ignore:
|
||||
- .pytest_cache/*
|
||||
31
core/modsecurity/files/coreruleset/.travis.yml
Normal file
31
core/modsecurity/files/coreruleset/.travis.yml
Normal file
@@ -0,0 +1,31 @@
|
||||
os: linux
|
||||
language: python
|
||||
python:
|
||||
- 2.7
|
||||
|
||||
# no more required in travis
|
||||
#sudo: required
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
||||
jobs:
|
||||
include:
|
||||
script:
|
||||
- |
|
||||
if [[ "$TRAVIS_PULL_REQUEST" != "false" ]]; then
|
||||
docker run -ti --name crs-test --entrypoint /docker-entrypoint.sh -e REPO=$TRAVIS_PULL_REQUEST_SLUG -e BRANCH=$TRAVIS_PULL_REQUEST_BRANCH themiddle/crs-test
|
||||
else
|
||||
docker run -ti --name crs-test --entrypoint /docker-entrypoint.sh -e REPO=$TRAVIS_REPO_SLUG -e BRANCH=$TRAVIS_BRANCH themiddle/crs-test
|
||||
fi
|
||||
|
||||
# safelist
|
||||
branches:
|
||||
only:
|
||||
- v3.1/dev
|
||||
- v3.2/dev
|
||||
- v3.3/dev
|
||||
- fix-travis
|
||||
|
||||
#notifications:
|
||||
# irc: "chat.freenode.net#modsecurity"
|
||||
18
core/modsecurity/files/coreruleset/.yamllint.yml
Normal file
18
core/modsecurity/files/coreruleset/.yamllint.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
extends: default
|
||||
|
||||
rules:
|
||||
# Test lines can be big
|
||||
line-length:
|
||||
max: 1024
|
||||
level: warning
|
||||
# These files below have very large lines, needed for the test.
|
||||
# So they will raise warnings every time.
|
||||
ignore: |
|
||||
tests/regression/tests/REQUEST-920-PROTOCOL-ENFORCEMENT/920380.yaml
|
||||
tests/regression/tests/REQUEST-920-PROTOCOL-ENFORCEMENT/920390.yaml
|
||||
tests/regression/tests/REQUEST-941-APPLICATION-ATTACK-XSS/941360.yaml
|
||||
|
||||
# don't bother me with this rule
|
||||
indentation: disable
|
||||
|
||||
comments: {require-starting-space: false}
|
||||
1443
core/modsecurity/files/coreruleset/CHANGES
Normal file
1443
core/modsecurity/files/coreruleset/CHANGES
Normal file
File diff suppressed because it is too large
Load Diff
152
core/modsecurity/files/coreruleset/CONTRIBUTING.md
Normal file
152
core/modsecurity/files/coreruleset/CONTRIBUTING.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# Contributing to the CRS
|
||||
|
||||
We value third-party contributions. To keep things simple for you and us,
|
||||
please adhere to the following contributing guidelines.
|
||||
|
||||
## Getting Started
|
||||
|
||||
* You will need a [GitHub account](https://github.com/signup/free).
|
||||
* Submit a [ticket for your issue](https://github.com/SpiderLabs/owasp-modsecurity-crs/issues), assuming one does not already exist.
|
||||
* Clearly describe the issue including steps to reproduce when it is a bug.
|
||||
* Make sure you specify the version that you know has the issue.
|
||||
* Bonus points for submitting a failing test along with the ticket.
|
||||
* If you don't have push access, fork the repository on GitHub.
|
||||
|
||||
## Making Changes
|
||||
|
||||
* Please base your changes on branch ```v3.3/dev```
|
||||
* Create a topic branch for your feature or bug fix.
|
||||
* Please fix only one problem at a time; this will help to quickly test and merge your change. If you intend to fix multiple unrelated problems, please use a separate branch for each problem.
|
||||
* Make commits of logical units.
|
||||
* Make sure your commits adhere to the rules guidelines below.
|
||||
* Make sure your commit messages are in the [proper format](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html): The first line of the message should have 50 characters or less, separated by a blank line from the (optional) body. The body should be wrapped at 70 characters and paragraphs separated by blank lines. Bulleted lists are also fine.
|
||||
|
||||
## General Formatting Guidelines for rules contributions
|
||||
|
||||
- 4 spaces per indentation level, no tabs
|
||||
- no trailing whitespace at EOL or trailing blank lines at EOF
|
||||
- comments are good, especially when they clearly explain the rule
|
||||
- try to adhere to a 80 character line length limit
|
||||
- if it is a [chained rule](https://github.com/SpiderLabs/ModSecurity/wiki/Reference-Manual#chain), alignment should be like
|
||||
```
|
||||
SecRule .. ..\
|
||||
"...."
|
||||
SecRule .. ..\
|
||||
"..."
|
||||
SecRule .. ..\
|
||||
".."
|
||||
```
|
||||
- use quotes even if there is only one action, it improves readability (e.g., use `"chain"`, not `chain`, or `"ctl:requestBodyAccess=Off"` instead of `ctl:requestBodyAccess=Off`)
|
||||
- always use numbers for phases, instead of names
|
||||
- format your `SecMarker` between double quotes, using UPPERCASE and separating words using hyphens. Examples are:
|
||||
```
|
||||
SecMarker "END-RESPONSE-959-BLOCKING-EVALUATION"
|
||||
SecMarker "END-REQUEST-910-IP-REPUTATION"
|
||||
```
|
||||
- the proposed order for actions is:
|
||||
```
|
||||
id
|
||||
phase
|
||||
allow | block | deny | drop | pass | proxy | redirect
|
||||
status
|
||||
capture
|
||||
t:xxx
|
||||
log
|
||||
nolog
|
||||
auditlog
|
||||
noauditlog
|
||||
msg
|
||||
logdata
|
||||
tag
|
||||
sanitiseArg
|
||||
sanitiseRequestHeader
|
||||
sanitiseMatched
|
||||
sanitiseMatchedBytes
|
||||
ctl
|
||||
ver
|
||||
severity
|
||||
multiMatch
|
||||
initcol
|
||||
setenv
|
||||
setvar
|
||||
expirevar
|
||||
chain
|
||||
skip
|
||||
skipAfter
|
||||
```
|
||||
|
||||
## Variable naming conventions
|
||||
|
||||
* Variable names are lowercase using chars from `[a-z0-9_]`
|
||||
* To somewhat reflect the fact that the syntax for variable usage is different when you define it (using setvar) and when you use it, we propose the following visual distinction:
|
||||
* Lowercase letters for collection, dot as separator, variable name. E.g.,: `setvar:tx.foo_bar_variable`
|
||||
* Capital letters for collection, colon as separator, variable name. E.g.,: `SecRule TX:foo_bar_variable`
|
||||
|
||||
## Rules compliance with each Paranoia Level (PL)
|
||||
|
||||
Rules in the CRS are organized in Paranoia Levels, which allows you to choose the desired level of rule checks.
|
||||
|
||||
Please read file ```crs-setup.conf.example``` for an introduction and a more detailed explanation of Paranoia Levels in the section `# -- [[ Paranoia Level Initialization ]]`.
|
||||
|
||||
**PL0:**
|
||||
|
||||
* Modsec installed, but almost no rules
|
||||
|
||||
**PL1:**
|
||||
|
||||
* Default level, keep in mind that most installations will normally use this one
|
||||
* If there is a complex memory consuming/evaluation rule it surely will be on upper levels, not this one
|
||||
* Normally we will use atomic checks in single rules
|
||||
* Confirmed matches only, all scores are allowed
|
||||
* No false positives / Low FP (Try to avoid adding rules with potential false positives!)
|
||||
* False negatives could happen
|
||||
|
||||
**PL2:**
|
||||
|
||||
* Chains usage are OK
|
||||
* Confirmed matches use score critical
|
||||
* Matches that cause false positives are limited to use score notice or warning
|
||||
* Low False positive rates
|
||||
* False negatives are not desirable
|
||||
|
||||
**PL3:**
|
||||
|
||||
* Chains usage with complex regex look arounds and macro expansions
|
||||
* Confirmed matches use score warning or critical
|
||||
* Matches that cause false positives are limited to use score notice
|
||||
* False positive rates increased but limited to multiple matches (not single string)
|
||||
* False negatives should be a very unlikely accident
|
||||
|
||||
**PL4:**
|
||||
|
||||
* Every item is inspected
|
||||
* Variable creations allowed to avoid engine limitations
|
||||
* Confirmed matches use score notice, warning or critical
|
||||
* Matches that cause false positives are limited to use score notice and warning
|
||||
* False positive rates increased (even on single string)
|
||||
* False negatives should not happen here
|
||||
* Check everything against RFC and white listed values for most popular elements
|
||||
|
||||
|
||||
## ID Numbering Scheme
|
||||
|
||||
The CRS project used the numerical id rule namespace from 900,000 to 999,999 for the CRS rules as well as 9,000,000 to 9,999,999 for default CRS rule exclusion packages.
|
||||
|
||||
Rules applying to the incoming request use the id range 900,000 to 949,999.
|
||||
Rules applying to the outgoing response use the id range 950,000 to 999,999.
|
||||
|
||||
The rules are grouped by vulnerability class they address (SQLi, RCE, etc.) or functionality (initialization). These groups occupy blocks of thousands (e.g. SQLi: 942,000 - 942,999).
|
||||
The grouped rules are defined in files dedicated to a single group or functionality. The filename takes up the first three digits of the rule ids defined within the file (e.g. SQLi: REQUEST-942-APPLICATION-ATTACK-SQLI.conf).
|
||||
|
||||
The individual rule files for the vulnerability classes are organized by the paranoia level of the rules. PL 1 is first, then PL 2 etc.
|
||||
|
||||
The block from 9XX000 - 9XX099 is reserved for use by CRS helper functionality. There are no blocking or filtering rules in this block.
|
||||
|
||||
Among the rules serving a CRS helper functionality are rules that skip rules depending on the paranoia level. These rules always use the following reserved rule ids: 9XX011-9XX018 with very few exceptions.
|
||||
|
||||
The blocking or filter rules start with 9XX100 with a step width of 10. E.g. 9XX100, 9XX110, 9XX120 etc. The rule id does not correspond directly with the paranoia level of a rule. Given the size of a rule group and the organization by lower PL rules first, PL2 and above tend to have rule IDs with higher numbers.
|
||||
|
||||
Within a rule file / block, there are sometimes smaller groups of rules that belong to together. They are closely linked and very often represent copies of the original rules with a stricter limit (alternatively, they can represent the same rule addressing a different target in a second rule where this was necessary). These are stricter siblings of the base rule. Stricter siblings usually share the first five digits of the rule ID and raise the rule ID by one. E.g., Base rule at 9XX160, stricter sibling at 9XX161.
|
||||
|
||||
Stricter siblings often have a different paranoia level. This means that the base rule and the stricter sibling do not reside next to one another in the rule file. Instead they are ordered in their appropriate paranoia level and can be linked via the first digits of the rule id. It is a good practice to introduce stricter siblings together with the base rule in the comments of the base rule and to reference the base rule with the keyword stricter sibling in the comments of the stricter sibling. E.g., "... This is
|
||||
performed in two separate stricter siblings of this rule: 9XXXX1 and 9XXXX2", "This is a stricter sibling of rule 9XXXX0."
|
||||
85
core/modsecurity/files/coreruleset/CONTRIBUTORS.md
Normal file
85
core/modsecurity/files/coreruleset/CONTRIBUTORS.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Contributors
|
||||
|
||||
## Project Co-Leads:
|
||||
|
||||
- [Chaim Sanders](https://github.com/csanders-git)
|
||||
- [Christian Folini](https://github.com/dune73)
|
||||
- [Walter Hop](https://github.com/lifeforms)
|
||||
|
||||
## Developers:
|
||||
|
||||
- [Franziska Bühler](https://github.com/franbuehler)
|
||||
- [Christoph Hansen](https://github.com/emphazer)
|
||||
- [Ervin Hegedus](https://github.com/airween)
|
||||
- [Victor Hora](https://github.com/victorhora)
|
||||
- [Andrea Menin](https://github.com/theMiddleBlue)
|
||||
- [Federico G. Schwindt](https://github.com/fgsch)
|
||||
- [Manuel Spartan](https://github.com/spartantri)
|
||||
- [Felipe Zimmerle](https://github.com/zimmerle)
|
||||
- [Felipe Zipitría](https://github.com/fzipi)
|
||||
|
||||
## Contributors:
|
||||
|
||||
- [agusmu](https://github.com/agusmu)
|
||||
- [Zack Allen](https://github.com/zmallen)
|
||||
- [azhao155](https://github.com/azhao155)
|
||||
- [azurit](https://github.com/azurit)
|
||||
- [Matt Bagley](https://github.com/bagley)
|
||||
- [Ryan Barnett](https://github.com/rcbarnett)
|
||||
- [soufianebenali](https://github.com/soufianebenali)
|
||||
- [Peter Bittner](https://github.com/bittner)
|
||||
- [Allan Boll](https://github.com/allanbomsft)
|
||||
- [Jeremy Brown](https://github.com/jwbrown77)
|
||||
- [Brent Clark](https://github.com/brentclark)
|
||||
- [Jonathan Claudius](https://github.com/claudijd)
|
||||
- [coolt](https://github.com/coolt)
|
||||
- [Ashish Dixit](https://github.com/tundal45)
|
||||
- [Padraig Doran](https://github.com/padraigdoran)
|
||||
- [Dan Ehrlich](https://github.com/danehrlich1)
|
||||
- [Umar Farook](https://github.com/umarfarook882)
|
||||
- [FrozenSolid](https://github.com/frozenSolid)
|
||||
- [Pásztor Gábor](https://github.com/gpasztor87)
|
||||
- [Aaron Haaf](https://github.com/Everspace)
|
||||
- [Michael Haas](https://github.com/MichaelHaas)
|
||||
- [jamuse](https://github.com/jamuse)
|
||||
- [jeremyjpj0916](https://github.com/jeremyjpj0916)
|
||||
- [jschleus](https://github.com/jschleus)
|
||||
- [Krzysztof Kotowicz](https://github.com/koto)
|
||||
- [Max Leske](https://github.com/theseion)
|
||||
- Manuel Leos
|
||||
- [Evgeny Marmalstein](https://github.com/shimshon70)
|
||||
- [meetug](https://github.com/meetug)
|
||||
- [Christian Mehlmauer](https://github.com/FireFart)
|
||||
- [Glyn Mooney](https://github.com/skidoosh)
|
||||
- [na1ex](https://github.com/na1ex)
|
||||
- [Jose Nazario](https://github.com/paralax)
|
||||
- [Tim Herren](https://github.com/nerrehmit)
|
||||
- [Scott O'Neil](https://github.com/cPanelScott)
|
||||
- [Fernando Outeda](https://github.com/fog94)
|
||||
- [NullIsNot0](https://github.com/NullIsNot0)
|
||||
- [Robert Paprocki](https://github.com/p0pr0ck5)
|
||||
- [Christian Peron](https://github.com/csjperon)
|
||||
- [Elia Pinto](https://github.com/yersinia)
|
||||
- [pyllyukko](https://github.com/pyllyukko)
|
||||
- [Brian Rectanus](https://github.com/b1v1r)
|
||||
- [Rufus125](https://github.com/Rufus125)
|
||||
- Ofer Shezaf
|
||||
- Breno Silva
|
||||
- siric\_
|
||||
- Emile-Hugo Spir
|
||||
- [Marc Stern](https://github.com/marcstern)
|
||||
- [Simon Studer](https://github.com/studersi)
|
||||
- [supplient](https://github.com/supplient)
|
||||
- [theMiddle](https://github.com/theMiddleBlue)
|
||||
- [Ben Williams](https://github.com/benwilliams)
|
||||
- [Anna Winkler](https://github.com/annawinkler)
|
||||
- [Avery Wong](https://github.com/4v3r9)
|
||||
- [Will Woodson](https://github.com/wjwoodson)
|
||||
- [Greg Wroblewski](https://github.com/gwroblew)
|
||||
- [XeroChen](https://github.com/XeroChen)
|
||||
- [ygrek](https://github.com/ygrek)
|
||||
- [Yu Yagihashi](https://github.com/yagihash)
|
||||
- [Zino](https://github.com/zinoe)
|
||||
- Josh Zlatin
|
||||
- [Zou Guangxian](https://github.com/zouguangxian)
|
||||
- [4ft35t](https://github.com/4ft35t)
|
||||
305
core/modsecurity/files/coreruleset/INSTALL
Normal file
305
core/modsecurity/files/coreruleset/INSTALL
Normal file
@@ -0,0 +1,305 @@
|
||||
_____ _____ _____ ____
|
||||
/ ____| __ \ / ____| |___ \
|
||||
| | | |__) | (___ __) |
|
||||
| | | _ / \___ \ |__ <
|
||||
| |____| | \ \ ____) | ___) |
|
||||
\_____|_| \_\_____/ |____/
|
||||
|
||||
OWASP Core Rule Set 3.x
|
||||
|
||||
Installing ModSecurity
|
||||
=====================
|
||||
|
||||
This document does NOT detail how to install ModSecurity. Rather,
|
||||
only information pertaining to the installation of the OWASP Core
|
||||
Rule Set (CRS) is provided. However, ModSecurity is a prerequisite
|
||||
for the CRS installation. Information on installing ModSecurity
|
||||
can be found within the ModSecurity project at
|
||||
https://github.com/SpiderLabs/ModSecurity or at ModSecurity.org.
|
||||
|
||||
Installing From a Package Manager
|
||||
=================================
|
||||
|
||||
The OWASP Core Rule Set (CRS) is available from many sources. On
|
||||
multiple platforms this includes package managers. These packages are
|
||||
maintained by independent packagers who package CRS in their own time.
|
||||
Historically, many of these packages have been out of date. As such,
|
||||
it is recommended that you install, where possible, from our GitHub
|
||||
repository. The following CRS 3.x packages are known to exist:
|
||||
|
||||
modsecurity-crs - Debian
|
||||
mod_security_crs - Fedora
|
||||
modsecurity-crs - Gentoo
|
||||
|
||||
Packages of CRS 2.x are incompatible with CRS 3.x.
|
||||
|
||||
Installing
|
||||
==========
|
||||
|
||||
You can download a copy of the CRS from the following URL:
|
||||
https://coreruleset.org/installation/
|
||||
|
||||
Our release zip/tar.gz files are the preferred way to install CRS.
|
||||
|
||||
However, if you want to follow rule development closely and get
|
||||
the newest protections quickly, you can also clone our GitHub
|
||||
repository to get the current work-in-progress for the next release.
|
||||
|
||||
Prerequisites
|
||||
-------------
|
||||
|
||||
CRS is designed to be used with ModSecurity (although many other
|
||||
projects also use the provided rules). CRS version 3.x is designed for
|
||||
ModSecurity 2.8 or above. CRS version 3.x makes use of libinjection
|
||||
and libXML2. Failure to provide these prerequisites may result in
|
||||
serious false negatives and CRS version 3.x should NOT be run without
|
||||
these. Note, however, that libinjection is bundled with ModSecurity
|
||||
since version 2.8. Additionally, if you are downloading from the
|
||||
GitHub repo you will need to install 'git' on your system.
|
||||
|
||||
Upgrading from CRS 2.x
|
||||
----------------------
|
||||
CRS 3.x is a major release incompatible with CRS 2.x.
|
||||
The rule IDs have changed. The file id_renumbering/IdNumbering.csv
|
||||
contains a list with old and new rule IDs. However, a key feature
|
||||
of the release 3.x is the reduction of false positives in the
|
||||
default installation and we recommend you start with a fresh
|
||||
install from scratch.
|
||||
Key parameter variables have changed their name and new features
|
||||
have been introduced. Your former modsecurity_crs_10_setup.conf
|
||||
file is thus no longer usable.
|
||||
We recommend you to start with a fresh install from scratch.
|
||||
|
||||
Installing on Apache
|
||||
--------------------
|
||||
1. Install ModSecurity for Apache
|
||||
2. Ensure that ModSecurity is loading correctly by checking error.log
|
||||
at start up for lines indicating ModSecurity is installed. An example
|
||||
might appear as follows:
|
||||
```ModSecurity for Apache/2.9.1 (http://www.modsecurity.org/) configured.```
|
||||
3. The most common method of deploying ModSecurity we have seen is
|
||||
to create a new folder underneath the Apache directory (typically
|
||||
/usr/local/apache/, /etc/httpd/, or /etc/apache2). Often this folder
|
||||
is called 'modsecurity.d'. Create this folder and cd into it.
|
||||
4. Download our release from https://coreruleset.org/installation/
|
||||
and unpack it into a new owasp-modsecurity-crs folder.
|
||||
5. Move the crs-setup.conf.example file to crs-setup.conf.
|
||||
Please take the time to go through this file and customize the settings
|
||||
for your local environment. Failure to do so may result in false
|
||||
negatives and false positives. See the section entitled OWASP CRS
|
||||
Configuration for more detail.
|
||||
6. Rename rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf.example and
|
||||
rules/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf.example to remove the
|
||||
'.example' extension. This will allow you to add exclusions without updates
|
||||
overwriting them in the future.
|
||||
7. Add the following line to your httpd.conf/apache2.conf (the following
|
||||
assumes you've put CRS into modsecurity.d/owasp-modsecurity-crs). You
|
||||
can alternatively place these in any config file included by Apache:
|
||||
```
|
||||
<IfModule security2_module>
|
||||
Include modsecurity.d/owasp-modsecurity-crs/crs-setup.conf
|
||||
Include modsecurity.d/owasp-modsecurity-crs/rules/*.conf
|
||||
</IfModule>
|
||||
```
|
||||
8. Restart web server and ensure it starts without errors
|
||||
9. Make sure your web sites are still running fine.
|
||||
10. Proceed to the section "Testing the Installation" below.
|
||||
|
||||
Installing on Nginx
|
||||
-------------------
|
||||
1. Compile ModSecurity into Nginx
|
||||
2. Ensure that ModSecurity is loading correctly by checking error.log
|
||||
at start up for lines indicating ModSecurity is installed. An example
|
||||
might appear as follows:
|
||||
```ModSecurity for nginx (STABLE)/2.9.1 (http://www.modsecurity.org/) configured.```
|
||||
3. The most common method of deploying ModSecurity we have seen is
|
||||
to create a new folder underneath the Nginx directory (typically
|
||||
/usr/local/nginx/conf/). Often this folder
|
||||
is called 'owasp-modsecurity-crs'. Create this folder and cd into it.
|
||||
4. Download our release from https://coreruleset.org/installation/
|
||||
and unpack it into a new owasp-modsecurity-crs folder.
|
||||
5. Move the crs-setup.conf.example file to crs-setup.conf.
|
||||
Please take this time to go through this
|
||||
file and customize the settings for your local environment. Failure to
|
||||
do so may result in false negatives and false positives. See the
|
||||
section entitled OWASP CRS Configuration for more detail.
|
||||
6. Rename rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf.example and
|
||||
rules/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf.example to remove the
|
||||
'.example' extension. This will allow you to add exceptions without updates
|
||||
overwriting them in the future.
|
||||
7. Nginx requires the configuration of a single ModSecurity
|
||||
configuration file within the nginx.conf file using the
|
||||
'ModSecurityConfig' directive (when using ModSecurity 2.x).
|
||||
Best practice is to set 'ModSecurityConfig' to a file from
|
||||
which you will include your other ModSecurity configuration
|
||||
files. In this example we will use:
|
||||
```ModSecurityConfig modsec_includes.conf;```
|
||||
7. Within modsec_includes.conf create your includes to the
|
||||
CRS folder similar to as follows (The modsecurity.conf file from the
|
||||
ModSecurity installation is included in this example):
|
||||
```
|
||||
include modsecurity.conf
|
||||
include owasp-modsecurity-crs/crs-setup.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-901-INITIALIZATION.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-903.9001-DRUPAL-EXCLUSION-RULES.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-903.9002-WORDPRESS-EXCLUSION-RULES.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-903.9003-NEXTCLOUD-EXCLUSION-RULES.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-903.9004-DOKUWIKI-EXCLUSION-RULES.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-903.9005-CPANEL-EXCLUSION-RULES.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-903.9006-XENFORO-EXCLUSION-RULES.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-905-COMMON-EXCEPTIONS.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-910-IP-REPUTATION.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-911-METHOD-ENFORCEMENT.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-912-DOS-PROTECTION.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-913-SCANNER-DETECTION.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-920-PROTOCOL-ENFORCEMENT.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-921-PROTOCOL-ATTACK.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-930-APPLICATION-ATTACK-LFI.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-931-APPLICATION-ATTACK-RFI.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-932-APPLICATION-ATTACK-RCE.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-933-APPLICATION-ATTACK-PHP.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-934-APPLICATION-ATTACK-NODEJS.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-941-APPLICATION-ATTACK-XSS.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-942-APPLICATION-ATTACK-SQLI.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-943-APPLICATION-ATTACK-SESSION-FIXATION.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-944-APPLICATION-ATTACK-JAVA.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-949-BLOCKING-EVALUATION.conf
|
||||
include owasp-modsecurity-crs/rules/RESPONSE-950-DATA-LEAKAGES.conf
|
||||
include owasp-modsecurity-crs/rules/RESPONSE-951-DATA-LEAKAGES-SQL.conf
|
||||
include owasp-modsecurity-crs/rules/RESPONSE-952-DATA-LEAKAGES-JAVA.conf
|
||||
include owasp-modsecurity-crs/rules/RESPONSE-953-DATA-LEAKAGES-PHP.conf
|
||||
include owasp-modsecurity-crs/rules/RESPONSE-954-DATA-LEAKAGES-IIS.conf
|
||||
include owasp-modsecurity-crs/rules/RESPONSE-959-BLOCKING-EVALUATION.conf
|
||||
include owasp-modsecurity-crs/rules/RESPONSE-980-CORRELATION.conf
|
||||
include owasp-modsecurity-crs/rules/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf
|
||||
```
|
||||
8. Restart web server and ensure it starts without errors
|
||||
9. Make sure your web sites are still running fine.
|
||||
10. Proceed to the section "Testing the Installation" below.
|
||||
|
||||
Installing on IIS
|
||||
-----------------
|
||||
The IIS installer comes with an optional version of CRS built in.
|
||||
To upgrade or install this after the fact follow the following
|
||||
steps.
|
||||
1. Navigate to "[drive_letters]:\Program Files\ModSecurity IIS\"
|
||||
2. Download our release from https://coreruleset.org/installation/
|
||||
and unpack it into the current folder.
|
||||
3. Move the crs-setup.conf.example file to crs-setup.conf.
|
||||
Please take this time to go through this
|
||||
file and customize the settings for your local environment. Failure to
|
||||
do so may result in false negatives and false positives. See the
|
||||
section entitled OWASP CRS Configuration for more detail.
|
||||
4. Rename rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf.example and
|
||||
rules/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf.example to remove the
|
||||
'.example' extension. This will allow you to add exceptions without updates
|
||||
overwriting them in the future.
|
||||
5. Navigate back to the 'ModSecurity IIS' folder and modify the
|
||||
'modsecurity_iis' to include the following:
|
||||
```
|
||||
include owasp-modsecurity-crs/crs-setup.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-901-INITIALIZATION.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-905-COMMON-EXCEPTIONS.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-910-IP-REPUTATION.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-911-METHOD-ENFORCEMENT.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-912-DOS-PROTECTION.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-913-SCANNER-DETECTION.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-920-PROTOCOL-ENFORCEMENT.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-921-PROTOCOL-ATTACK.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-930-APPLICATION-ATTACK-LFI.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-931-APPLICATION-ATTACK-RFI.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-932-APPLICATION-ATTACK-RCE.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-933-APPLICATION-ATTACK-PHP.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-941-APPLICATION-ATTACK-XSS.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-942-APPLICATION-ATTACK-SQLI.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-943-APPLICATION-ATTACK-SESSION-FIXATION.conf
|
||||
include owasp-modsecurity-crs/rules/REQUEST-949-BLOCKING-EVALUATION.conf
|
||||
include owasp-modsecurity-crs/rules/RESPONSE-950-DATA-LEAKAGES.conf
|
||||
include owasp-modsecurity-crs/rules/RESPONSE-951-DATA-LEAKAGES-SQL.conf
|
||||
include owasp-modsecurity-crs/rules/RESPONSE-952-DATA-LEAKAGES-JAVA.conf
|
||||
include owasp-modsecurity-crs/rules/RESPONSE-953-DATA-LEAKAGES-PHP.conf
|
||||
include owasp-modsecurity-crs/rules/RESPONSE-954-DATA-LEAKAGES-IIS.conf
|
||||
include owasp-modsecurity-crs/rules/RESPONSE-959-BLOCKING-EVALUATION.conf
|
||||
include owasp-modsecurity-crs/rules/RESPONSE-980-CORRELATION.conf
|
||||
include owasp-modsecurity-crs/rules/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf
|
||||
```
|
||||
6. Restart web server and ensure it starts without errors
|
||||
7. Make sure your web sites are still running fine.
|
||||
8. Proceed to the section "Testing the Installation" below.
|
||||
|
||||
Testing the Installation
|
||||
========================
|
||||
To test your installation you should be able to use any number
|
||||
of attacks. A typical request which should trigger CRS would be
|
||||
```http://localhost/?param="><script>alert(1);</script>```
|
||||
Upon sending this request you should see events reported in the
|
||||
error log (nginx apache) or the event viewer (IIS).
|
||||
|
||||
If have not changed the defaults with regards to anomaly scoring,
|
||||
blocking and sampling percentage, then this request should have
|
||||
been blocked and access forbidden. Likewise if you have configured
|
||||
ModSecurity debug logging and/or audit logging this event should
|
||||
log to these locations as well.
|
||||
|
||||
OWASP CRS Configuration
|
||||
=======================
|
||||
The crs-setup.conf file includes management rules
|
||||
and directives that can control important CRS functions.
|
||||
The crs-setup.conf file comes with extensive comments.
|
||||
This section here brings only the essential parts.
|
||||
|
||||
By default we do not include settings within the crs-setup.conf
|
||||
that configure ModSecurity itself. Instead those configuration
|
||||
settings are set during the installation of ModSecurity proper.
|
||||
An example for such such a
|
||||
configuration file is available via the ModSecurity project
|
||||
(https://github.com/SpiderLabs/ModSecurity/blob/master/modsecurity.conf-recommended).
|
||||
Be aware the crs-setup.conf file DOES specify
|
||||
configuration directives such as SecDefaultAction. The default
|
||||
is the anomaly scoring mode with the appropriate
|
||||
SecDefaultAction as defined in the crs-setup.conf.
|
||||
Alternative configuration modes are supported and explained
|
||||
in crs-setup.conf.
|
||||
|
||||
The default anomaly/correlation mode establishes an incoming
|
||||
anomaly score threshold of 5 and an outgoing anomaly score
|
||||
threshold of 4. The default installation has been tuned to
|
||||
reduce false positives in a way that will allow most requests
|
||||
to pass in this default setup.
|
||||
|
||||
However, testing the setup and tuning false positives
|
||||
before going to production is vital. This is especially true
|
||||
if you raise the paranoia level with is set to 1 by default.
|
||||
Higher paranoia levels ranging from 2 to 4 include more
|
||||
aggressive rules which will raise additional false positives
|
||||
but also raise the security level of your service.
|
||||
|
||||
If you are unsure about the performance impact of the CRS
|
||||
or if you are unsure about the number of false positives, then
|
||||
you may want to use the sampling percentage. This number,
|
||||
which is set to 100 by default, controls the percentage
|
||||
of requests which is funneled into the CRS. Fresh installs
|
||||
on high traffic sites are advised to start with a low, or
|
||||
very low number of percentages and raise the number
|
||||
slowly up to 100. Be aware that any number below 100 allows
|
||||
a random number of requests to bypass the ruleset completely.
|
||||
|
||||
Update the TX policy settings for allowed Request Methods, File
|
||||
Extensions, maximum numbers of arguments, etc to better reflect
|
||||
your environment that is being protected.
|
||||
|
||||
Make sure your GeoIP and Project Honeypot settings are specified
|
||||
if you are using them.
|
||||
The GeoIP database is no longer included with the CRS. Instead
|
||||
you are advised to download it regularly.
|
||||
|
||||
The use of Project Honeypot requires a
|
||||
free API key. These require an account but can be obtained at
|
||||
https://www.projecthoneypot.org/httpbl_configure.php.
|
||||
|
||||
Be sure to check out the other settings present within the
|
||||
crs-setup.conf file. There are many other options that have to
|
||||
do with aspects of web application security that are beyond
|
||||
this document but are well explained in crs-setup.conf.
|
||||
50
core/modsecurity/files/coreruleset/KNOWN_BUGS
Normal file
50
core/modsecurity/files/coreruleset/KNOWN_BUGS
Normal file
@@ -0,0 +1,50 @@
|
||||
== OWASP ModSecurity Core Rule Set (CRS) KNOWN BUGS ==
|
||||
|
||||
== Report Bugs/Issues to GitHub Issues Tracker or the mailinglist ==
|
||||
* https://github.com/SpiderLabs/owasp-modsecurity-crs/issues
|
||||
or the CRS Google Group at
|
||||
* https://groups.google.com/a/owasp.org/forum/#!forum/modsecurity-core-rule-set-project
|
||||
|
||||
* There are still false positives for standard web applications in
|
||||
the default install (paranoia level 1). Please report these when
|
||||
you encounter them.
|
||||
False Positives from paranoia level 2 rules are less interesting,
|
||||
as we expect users to write exclusion rules for their alerts in
|
||||
the higher paranoia levels.
|
||||
* Permanent blocking of clients is based on a previous user agent / IP
|
||||
combination. Changing the user agent will thus allow to bypass
|
||||
this new filter. The plan is to allow for a purely IP based
|
||||
filter in the future.
|
||||
* Apache 2.4 prior to 2.4.11 is affected by a bug in parsing multi-line
|
||||
configuration directives, which causes Apache to fail during startup
|
||||
with an error such as:
|
||||
Error parsing actions: Unknown action: \\
|
||||
Action 'configtest' failed.
|
||||
This bug is known to plague RHEL/Centos 7 below v7.4 or
|
||||
httpd v2.4.6 release 67 and Ubuntu 14.04 LTS users.
|
||||
https://bz.apache.org/bugzilla/show_bug.cgi?id=55910
|
||||
We advise to upgrade your Apache version. If upgrading is not possible,
|
||||
we have provided a script in the util/join-multiline-rules directory
|
||||
which converts the rules into a format that works around the bug.
|
||||
You have to re-run this script whenever you modify or update
|
||||
the CRS rules.
|
||||
* Debian up to and including Jessie lacks YAJL/JSON support in ModSecurity,
|
||||
which causes the following error in the Apache ErrorLog or SecAuditLog:
|
||||
'ModSecurity: JSON support was not enabled.'
|
||||
JSON support was enabled in Debian's package version 2.8.0-4 (Nov 2014).
|
||||
You can either use backports.debian.org to install the latest ModSecurity
|
||||
release or disable rule id 200001.
|
||||
* As of CRS version 3.0.1, support has been added for the application/soap+xml MIME
|
||||
type by default, as specified in RFC 3902. OF IMPORTANCE, application/soap+xml is
|
||||
indicative that XML will be provided. In accordance with this, ModSecurity's XML
|
||||
Request Body Processor should also be configured to support this MIME type. Within
|
||||
the ModSecurity project, commit 5e4e2af
|
||||
(https://github.com/SpiderLabs/ModSecurity/commit/5e4e2af7a6f07854fee6ed36ef4a381d4e03960e)
|
||||
has been merged to support this endeavour. However, if you are running a modified or
|
||||
preexisting version of the modsecurity.conf provided by this repository, you may
|
||||
wish to upgrade rule '200000' accordingly. The rule now appears as follows:
|
||||
|
||||
```
|
||||
SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\+|/)|text/)xml" \
|
||||
"id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML"
|
||||
```
|
||||
201
core/modsecurity/files/coreruleset/LICENSE
Normal file
201
core/modsecurity/files/coreruleset/LICENSE
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2006 the OWASP Core Rule Set contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
32
core/modsecurity/files/coreruleset/README.md
Normal file
32
core/modsecurity/files/coreruleset/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||

|
||||

|
||||

|
||||
[](https://owasp.org/projects/)
|
||||
[](https://bestpractices.coreinfrastructure.org/projects/1390)
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
|
||||
|
||||
|
||||
# OWASP ModSecurity Core Rule Set (CRS)
|
||||
|
||||
The OWASP ModSecurity Core Rule Set (CRS) is a set of generic attack detection rules for use with ModSecurity or compatible web application firewalls. The CRS aims to protect web applications from a wide range of attacks, including the OWASP Top Ten, with a minimum of false alerts.
|
||||
|
||||
## CRS Resources
|
||||
|
||||
Please see the [OWASP ModSecurity Core Rule Set page](https://coreruleset.org/) to get introduced to the CRS and view resources on installation, configuration, and working with the CRS.
|
||||
|
||||
## Contributing to the CRS
|
||||
|
||||
We strive to make the OWASP ModSecurity CRS accessible to a wide audience of beginner and experienced users. We are interested in hearing any bug reports, false positive alert reports, evasions, usability issues, and suggestions for new detections.
|
||||
|
||||
[Create an issue on GitHub](https://github.com/coreruleset/coreruleset/issues) to report a false positive or false negative (evasion). Please include your installed version and the relevant portions of your ModSecurity audit log.
|
||||
|
||||
[Sign up for our Google Group](https://groups.google.com/a/owasp.org/forum/#!forum/modsecurity-core-rule-set-project) to ask general usage questions and participate in discussions on the CRS. Also [here](https://lists.owasp.org/pipermail/owasp-modsecurity-core-rule-set/index) you can find the archives for the previous mailing list.
|
||||
|
||||
[Join the #coreruleset channel on OWASP Slack](http://owaspslack.com) to chat about the CRS.
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) 2006-2020 Trustwave and contributors. All rights reserved.
|
||||
|
||||
The OWASP ModSecurity Core Rule Set is distributed under Apache Software License (ASL) version 2. Please see the enclosed LICENSE file for full details.
|
||||
35
core/modsecurity/files/coreruleset/SECURITY.md
Normal file
35
core/modsecurity/files/coreruleset/SECURITY.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
OWASP CRS has two types of releases, Major releases (3.0.0, 3.1.0, 3.2.0 etc.) and point releases (3.0.1, 3.0.2 etc.).
|
||||
For more information see our [wiki](https://github.com/SpiderLabs/owasp-modsecurity-crs/wiki/Release-Policy).
|
||||
The OWASP CRS officially supports the two point releases with security patching preceding the current major release .
|
||||
We are happy to receive and merge PR's that address security issues in older versions of the project, but the team itself may choose not to fix these.
|
||||
Along those lines, OWASP CRS team may not issue security notifications for unsupported software.
|
||||
|
||||
| Version | Supported |
|
||||
| --------- | ------------------ |
|
||||
| 3.3.x-dev | :white_check_mark: |
|
||||
| 3.2.x | :white_check_mark: |
|
||||
| 3.1.x | :white_check_mark: |
|
||||
| 3.0.x | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
We strive to make the OWASP ModSecurity CRS accessible to a wide audience of beginner and experienced users.
|
||||
We welcome bug reports, false positive alert reports, evasions, usability issues, and suggestions for new detections.
|
||||
Submit these types of non-vulnerability related issues via Github.
|
||||
Please include your installed version and the relevant portions of your audit log.
|
||||
False negative or common bypasses should [create an issue](https://github.com/SpiderLabs/owasp-modsecurity-crs/issues/new) so they can be addressed.
|
||||
|
||||
Do this before submitting a vulnerability using our email:
|
||||
1) Verify that you have the latest version of OWASP CRS.
|
||||
2) Validate which Paranoia Level this bypass applies to. If it works in PL4, please send us an email.
|
||||
3) If you detected anything that causes unexpected behavior of the engine via manipulation of existing CRS provided rules, please send it by email.
|
||||
|
||||
Our email is [security@coreruleset.org](mailto:security@coreruleset.org). You can send us encrypted email using [this key](https://coreruleset.org/security.asc), (fingerprint: `3600 6F0E 0BA1 6783 2158 8211 38EE ACA1 AB8A 6E72`).
|
||||
|
||||
We are happy to work with the community to provide CVE identifiers for any discovered security issues if requested.
|
||||
|
||||
If in doubt, feel free to reach out to us!
|
||||
829
core/modsecurity/files/coreruleset/crs-setup.conf.example
Normal file
829
core/modsecurity/files/coreruleset/crs-setup.conf.example
Normal file
@@ -0,0 +1,829 @@
|
||||
# ------------------------------------------------------------------------
|
||||
# OWASP ModSecurity Core Rule Set ver.3.3.2
|
||||
# Copyright (c) 2006-2020 Trustwave and contributors. All rights reserved.
|
||||
#
|
||||
# The OWASP ModSecurity Core Rule Set is distributed under
|
||||
# Apache Software License (ASL) version 2
|
||||
# Please see the enclosed LICENSE file for full details.
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
|
||||
#
|
||||
# -- [[ Introduction ]] --------------------------------------------------------
|
||||
#
|
||||
# The OWASP ModSecurity Core Rule Set (CRS) is a set of generic attack
|
||||
# detection rules that provide a base level of protection for any web
|
||||
# application. They are written for the open source, cross-platform
|
||||
# ModSecurity Web Application Firewall.
|
||||
#
|
||||
# See also:
|
||||
# https://coreruleset.org/
|
||||
# https://github.com/SpiderLabs/owasp-modsecurity-crs
|
||||
# https://www.owasp.org/index.php/Category:OWASP_ModSecurity_Core_Rule_Set_Project
|
||||
#
|
||||
|
||||
|
||||
#
|
||||
# -- [[ System Requirements ]] -------------------------------------------------
|
||||
#
|
||||
# CRS requires ModSecurity version 2.8.0 or above.
|
||||
# We recommend to always use the newest ModSecurity version.
|
||||
#
|
||||
# The configuration directives/settings in this file are used to control
|
||||
# the OWASP ModSecurity CRS. These settings do **NOT** configure the main
|
||||
# ModSecurity settings (modsecurity.conf) such as SecRuleEngine,
|
||||
# SecRequestBodyAccess, SecAuditEngine, SecDebugLog, and XML processing.
|
||||
#
|
||||
# The CRS assumes that modsecurity.conf has been loaded. It is bundled with
|
||||
# ModSecurity. If you don't have it, you can get it from:
|
||||
# 2.x: https://raw.githubusercontent.com/SpiderLabs/ModSecurity/v2/master/modsecurity.conf-recommended
|
||||
# 3.x: https://raw.githubusercontent.com/SpiderLabs/ModSecurity/v3/master/modsecurity.conf-recommended
|
||||
#
|
||||
# The order of file inclusion in your webserver configuration should always be:
|
||||
# 1. modsecurity.conf
|
||||
# 2. crs-setup.conf (this file)
|
||||
# 3. rules/*.conf (the CRS rule files)
|
||||
#
|
||||
# Please refer to the INSTALL file for detailed installation instructions.
|
||||
#
|
||||
|
||||
|
||||
#
|
||||
# -- [[ Mode of Operation: Anomaly Scoring vs. Self-Contained ]] ---------------
|
||||
#
|
||||
# The CRS can run in two modes:
|
||||
#
|
||||
# -- [[ Anomaly Scoring Mode (default) ]] --
|
||||
# In CRS3, anomaly mode is the default and recommended mode, since it gives the
|
||||
# most accurate log information and offers the most flexibility in setting your
|
||||
# blocking policies. It is also called "collaborative detection mode".
|
||||
# In this mode, each matching rule increases an 'anomaly score'.
|
||||
# At the conclusion of the inbound rules, and again at the conclusion of the
|
||||
# outbound rules, the anomaly score is checked, and the blocking evaluation
|
||||
# rules apply a disruptive action, by default returning an error 403.
|
||||
#
|
||||
# -- [[ Self-Contained Mode ]] --
|
||||
# In this mode, rules apply an action instantly. This was the CRS2 default.
|
||||
# It can lower resource usage, at the cost of less flexibility in blocking policy
|
||||
# and less informative audit logs (only the first detected threat is logged).
|
||||
# Rules inherit the disruptive action that you specify (i.e. deny, drop, etc).
|
||||
# The first rule that matches will execute this action. In most cases this will
|
||||
# cause evaluation to stop after the first rule has matched, similar to how many
|
||||
# IDSs function.
|
||||
#
|
||||
# -- [[ Alert Logging Control ]] --
|
||||
# In the mode configuration, you must also adjust the desired logging options.
|
||||
# There are three common options for dealing with logging. By default CRS enables
|
||||
# logging to the webserver error log (or Event viewer) plus detailed logging to
|
||||
# the ModSecurity audit log (configured under SecAuditLog in modsecurity.conf).
|
||||
#
|
||||
# - To log to both error log and ModSecurity audit log file, use: "log,auditlog"
|
||||
# - To log *only* to the ModSecurity audit log file, use: "nolog,auditlog"
|
||||
# - To log *only* to the error log file, use: "log,noauditlog"
|
||||
#
|
||||
# Examples for the various modes follow.
|
||||
# You must leave one of the following options enabled.
|
||||
# Note that you must specify the same line for phase:1 and phase:2.
|
||||
#
|
||||
|
||||
# Default: Anomaly Scoring mode, log to error log, log to ModSecurity audit log
|
||||
# - By default, offending requests are blocked with an error 403 response.
|
||||
# - To change the disruptive action, see RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf.example
|
||||
# and review section 'Changing the Disruptive Action for Anomaly Mode'.
|
||||
# - In Apache, you can use ErrorDocument to show a friendly error page or
|
||||
# perform a redirect: https://httpd.apache.org/docs/2.4/custom-error.html
|
||||
#
|
||||
SecDefaultAction "phase:1,log,auditlog,pass"
|
||||
SecDefaultAction "phase:2,log,auditlog,pass"
|
||||
|
||||
# Example: Anomaly Scoring mode, log only to ModSecurity audit log
|
||||
# - By default, offending requests are blocked with an error 403 response.
|
||||
# - To change the disruptive action, see RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf.example
|
||||
# and review section 'Changing the Disruptive Action for Anomaly Mode'.
|
||||
# - In Apache, you can use ErrorDocument to show a friendly error page or
|
||||
# perform a redirect: https://httpd.apache.org/docs/2.4/custom-error.html
|
||||
#
|
||||
# SecDefaultAction "phase:1,nolog,auditlog,pass"
|
||||
# SecDefaultAction "phase:2,nolog,auditlog,pass"
|
||||
|
||||
# Example: Self-contained mode, return error 403 on blocking
|
||||
# - In this configuration the default disruptive action becomes 'deny'. After a
|
||||
# rule triggers, it will stop processing the request and return an error 403.
|
||||
# - You can also use a different error status, such as 404, 406, et cetera.
|
||||
# - In Apache, you can use ErrorDocument to show a friendly error page or
|
||||
# perform a redirect: https://httpd.apache.org/docs/2.4/custom-error.html
|
||||
#
|
||||
# SecDefaultAction "phase:1,log,auditlog,deny,status:403"
|
||||
# SecDefaultAction "phase:2,log,auditlog,deny,status:403"
|
||||
|
||||
# Example: Self-contained mode, redirect back to homepage on blocking
|
||||
# - In this configuration the 'tag' action includes the Host header data in the
|
||||
# log. This helps to identify which virtual host triggered the rule (if any).
|
||||
# - Note that this might cause redirect loops in some situations; for example
|
||||
# if a Cookie or User-Agent header is blocked, it will also be blocked when
|
||||
# the client subsequently tries to access the homepage. You can also redirect
|
||||
# to another custom URL.
|
||||
# SecDefaultAction "phase:1,log,auditlog,redirect:'http://%{request_headers.host}/',tag:'Host: %{request_headers.host}'"
|
||||
# SecDefaultAction "phase:2,log,auditlog,redirect:'http://%{request_headers.host}/',tag:'Host: %{request_headers.host}'"
|
||||
|
||||
|
||||
#
|
||||
# -- [[ Paranoia Level Initialization ]] ---------------------------------------
|
||||
#
|
||||
# The Paranoia Level (PL) setting allows you to choose the desired level
|
||||
# of rule checks that will add to your anomaly scores.
|
||||
#
|
||||
# With each paranoia level increase, the CRS enables additional rules
|
||||
# giving you a higher level of security. However, higher paranoia levels
|
||||
# also increase the possibility of blocking some legitimate traffic due to
|
||||
# false alarms (also named false positives or FPs). If you use higher
|
||||
# paranoia levels, it is likely that you will need to add some exclusion
|
||||
# rules for certain requests and applications receiving complex input.
|
||||
#
|
||||
# - A paranoia level of 1 is default. In this level, most core rules
|
||||
# are enabled. PL1 is advised for beginners, installations
|
||||
# covering many different sites and applications, and for setups
|
||||
# with standard security requirements.
|
||||
# At PL1 you should face FPs rarely. If you encounter FPs, please
|
||||
# open an issue on the CRS GitHub site and don't forget to attach your
|
||||
# complete Audit Log record for the request with the issue.
|
||||
# - Paranoia level 2 includes many extra rules, for instance enabling
|
||||
# many regexp-based SQL and XSS injection protections, and adding
|
||||
# extra keywords checked for code injections. PL2 is advised
|
||||
# for moderate to experienced users desiring more complete coverage
|
||||
# and for installations with elevated security requirements.
|
||||
# PL2 comes with some FPs which you need to handle.
|
||||
# - Paranoia level 3 enables more rules and keyword lists, and tweaks
|
||||
# limits on special characters used. PL3 is aimed at users experienced
|
||||
# at the handling of FPs and at installations with a high security
|
||||
# requirement.
|
||||
# - Paranoia level 4 further restricts special characters.
|
||||
# The highest level is advised for experienced users protecting
|
||||
# installations with very high security requirements. Running PL4 will
|
||||
# likely produce a very high number of FPs which have to be
|
||||
# treated before the site can go productive.
|
||||
#
|
||||
# All rules will log their PL to the audit log;
|
||||
# example: [tag "paranoia-level/2"]. This allows you to deduct from the
|
||||
# audit log how the WAF behavior is affected by paranoia level.
|
||||
#
|
||||
# It is important to also look into the variable
|
||||
# tx.enforce_bodyproc_urlencoded (Enforce Body Processor URLENCODED)
|
||||
# defined below. Enabling it closes a possible bypass of CRS.
|
||||
#
|
||||
# Uncomment this rule to change the default:
|
||||
#
|
||||
#SecAction \
|
||||
# "id:900000,\
|
||||
# phase:1,\
|
||||
# nolog,\
|
||||
# pass,\
|
||||
# t:none,\
|
||||
# setvar:tx.paranoia_level=1"
|
||||
|
||||
|
||||
# It is possible to execute rules from a higher paranoia level but not include
|
||||
# them in the anomaly scoring. This allows you to take a well-tuned system on
|
||||
# paranoia level 1 and add rules from paranoia level 2 without having to fear
|
||||
# the new rules would lead to false positives that raise your score above the
|
||||
# threshold.
|
||||
# This optional feature is enabled by uncommenting the following rule and
|
||||
# setting the tx.executing_paranoia_level.
|
||||
# Technically, rules up to the level defined in tx.executing_paranoia_level
|
||||
# will be executed, but only the rules up to tx.paranoia_level affect the
|
||||
# anomaly scores.
|
||||
# By default, tx.executing_paranoia_level is set to tx.paranoia_level.
|
||||
# tx.executing_paranoia_level must not be lower than tx.paranoia_level.
|
||||
#
|
||||
# Please notice that setting tx.executing_paranoia_level to a higher paranoia
|
||||
# level results in a performance impact that is equally high as setting
|
||||
# tx.paranoia_level to said level.
|
||||
#
|
||||
#SecAction \
|
||||
# "id:900001,\
|
||||
# phase:1,\
|
||||
# nolog,\
|
||||
# pass,\
|
||||
# t:none,\
|
||||
# setvar:tx.executing_paranoia_level=1"
|
||||
|
||||
|
||||
#
|
||||
# -- [[ Enforce Body Processor URLENCODED ]] -----------------------------------
|
||||
#
|
||||
# ModSecurity selects the body processor based on the Content-Type request
|
||||
# header. But clients are not always setting the Content-Type header for their
|
||||
# request body payloads. This will leave ModSecurity with limited vision into
|
||||
# the payload. The variable tx.enforce_bodyproc_urlencoded lets you force the
|
||||
# URLENCODED body processor in these situations. This is off by default, as it
|
||||
# implies a change of the behaviour of ModSecurity beyond CRS (the body
|
||||
# processor applies to all rules, not only CRS) and because it may lead to
|
||||
# false positives already on paranoia level 1. However, enabling this variable
|
||||
# closes a possible bypass of CRS so it should be considered.
|
||||
#
|
||||
# Uncomment this rule to change the default:
|
||||
#
|
||||
#SecAction \
|
||||
# "id:900010,\
|
||||
# phase:1,\
|
||||
# nolog,\
|
||||
# pass,\
|
||||
# t:none,\
|
||||
# setvar:tx.enforce_bodyproc_urlencoded=1"
|
||||
|
||||
|
||||
#
|
||||
# -- [[ Anomaly Mode Severity Levels ]] ----------------------------------------
|
||||
#
|
||||
# Each rule in the CRS has an associated severity level.
|
||||
# These are the default scoring points for each severity level.
|
||||
# These settings will be used to increment the anomaly score if a rule matches.
|
||||
# You may adjust these points to your liking, but this is usually not needed.
|
||||
#
|
||||
# - CRITICAL severity: Anomaly Score of 5.
|
||||
# Mostly generated by the application attack rules (93x and 94x files).
|
||||
# - ERROR severity: Anomaly Score of 4.
|
||||
# Generated mostly from outbound leakage rules (95x files).
|
||||
# - WARNING severity: Anomaly Score of 3.
|
||||
# Generated mostly by malicious client rules (91x files).
|
||||
# - NOTICE severity: Anomaly Score of 2.
|
||||
# Generated mostly by the protocol rules (92x files).
|
||||
#
|
||||
# In anomaly mode, these scores are cumulative.
|
||||
# So it's possible for a request to hit multiple rules.
|
||||
#
|
||||
# (Note: In this file, we use 'phase:1' to set CRS configuration variables.
|
||||
# In general, 'phase:request' is used. However, we want to make absolutely sure
|
||||
# that all configuration variables are set before the CRS rules are processed.)
|
||||
#
|
||||
#SecAction \
|
||||
# "id:900100,\
|
||||
# phase:1,\
|
||||
# nolog,\
|
||||
# pass,\
|
||||
# t:none,\
|
||||
# setvar:tx.critical_anomaly_score=5,\
|
||||
# setvar:tx.error_anomaly_score=4,\
|
||||
# setvar:tx.warning_anomaly_score=3,\
|
||||
# setvar:tx.notice_anomaly_score=2"
|
||||
|
||||
|
||||
#
|
||||
# -- [[ Anomaly Mode Blocking Threshold Levels ]] ------------------------------
|
||||
#
|
||||
# Here, you can specify at which cumulative anomaly score an inbound request,
|
||||
# or outbound response, gets blocked.
|
||||
#
|
||||
# Most detected inbound threats will give a critical score of 5.
|
||||
# Smaller violations, like violations of protocol/standards, carry lower scores.
|
||||
#
|
||||
# [ At default value ]
|
||||
# If you keep the blocking thresholds at the defaults, the CRS will work
|
||||
# similarly to previous CRS versions: a single critical rule match will cause
|
||||
# the request to be blocked and logged.
|
||||
#
|
||||
# [ Using higher values ]
|
||||
# If you want to make the CRS less sensitive, you can increase the blocking
|
||||
# thresholds, for instance to 7 (which would require multiple rule matches
|
||||
# before blocking) or 10 (which would require at least two critical alerts - or
|
||||
# a combination of many lesser alerts), or even higher. However, increasing the
|
||||
# thresholds might cause some attacks to bypass the CRS rules or your policies.
|
||||
#
|
||||
# [ New deployment strategy: Starting high and decreasing ]
|
||||
# It is a common practice to start a fresh CRS installation with elevated
|
||||
# anomaly scoring thresholds (>100) and then lower the limits as your
|
||||
# confidence in the setup grows. You may also look into the Sampling
|
||||
# Percentage section below for a different strategy to ease into a new
|
||||
# CRS installation.
|
||||
#
|
||||
# [ Anomaly Threshold / Paranoia Level Quadrant ]
|
||||
#
|
||||
# High Anomaly Limit | High Anomaly Limit
|
||||
# Low Paranoia Level | High Paranoia Level
|
||||
# -> Fresh Site | -> Experimental Site
|
||||
# ------------------------------------------------------
|
||||
# Low Anomaly Limit | Low Anomaly Limit
|
||||
# Low Paranoia Level | High Paranoia Level
|
||||
# -> Standard Site | -> High Security Site
|
||||
#
|
||||
# Uncomment this rule to change the defaults:
|
||||
#
|
||||
#SecAction \
|
||||
# "id:900110,\
|
||||
# phase:1,\
|
||||
# nolog,\
|
||||
# pass,\
|
||||
# t:none,\
|
||||
# setvar:tx.inbound_anomaly_score_threshold=5,\
|
||||
# setvar:tx.outbound_anomaly_score_threshold=4"
|
||||
|
||||
#
|
||||
# -- [[ Application Specific Rule Exclusions ]] ----------------------------------------
|
||||
#
|
||||
# Some well-known applications may undertake actions that appear to be
|
||||
# malicious. This includes actions such as allowing HTML or Javascript within
|
||||
# parameters. In such cases the CRS aims to prevent false positives by allowing
|
||||
# administrators to enable prebuilt, application specific exclusions on an
|
||||
# application by application basis.
|
||||
# These application specific exclusions are distinct from the rules that would
|
||||
# be placed in the REQUEST-900-EXCLUSION-RULES-BEFORE-CRS configuration file as
|
||||
# they are prebuilt for specific applications. The 'REQUEST-900' file is
|
||||
# designed for users to add their own custom exclusions. Note, using these
|
||||
# application specific exclusions may loosen restrictions of the CRS,
|
||||
# especially if used with an application they weren't designed for. As a result
|
||||
# they should be applied with care.
|
||||
# To use this functionality you must specify a supported application. To do so
|
||||
# uncomment rule 900130. In addition to uncommenting the rule you will need to
|
||||
# specify which application(s) you'd like to enable exclusions for. Only a
|
||||
# (very) limited set of applications are currently supported, please use the
|
||||
# filenames prefixed with 'REQUEST-903' to guide you in your selection.
|
||||
# Such filenames use the following convention:
|
||||
# REQUEST-903.9XXX-{APPNAME}-EXCLUSIONS-RULES.conf
|
||||
#
|
||||
# It is recommended if you run multiple web applications on your site to limit
|
||||
# the effects of the exclusion to only the path where the excluded webapp
|
||||
# resides using a rule similar to the following example:
|
||||
# SecRule REQUEST_URI "@beginsWith /wordpress/" setvar:tx.crs_exclusions_wordpress=1
|
||||
|
||||
#
|
||||
# Modify and uncomment this rule to select which application:
|
||||
#
|
||||
#SecAction \
|
||||
# "id:900130,\
|
||||
# phase:1,\
|
||||
# nolog,\
|
||||
# pass,\
|
||||
# t:none,\
|
||||
# setvar:tx.crs_exclusions_cpanel=1,\
|
||||
# setvar:tx.crs_exclusions_drupal=1,\
|
||||
# setvar:tx.crs_exclusions_dokuwiki=1,\
|
||||
# setvar:tx.crs_exclusions_nextcloud=1,\
|
||||
# setvar:tx.crs_exclusions_wordpress=1,\
|
||||
# setvar:tx.crs_exclusions_xenforo=1"
|
||||
|
||||
#
|
||||
# -- [[ HTTP Policy Settings ]] ------------------------------------------------
|
||||
#
|
||||
# This section defines your policies for the HTTP protocol, such as:
|
||||
# - allowed HTTP versions, HTTP methods, allowed request Content-Types
|
||||
# - forbidden file extensions (e.g. .bak, .sql) and request headers (e.g. Proxy)
|
||||
#
|
||||
# These variables are used in the following rule files:
|
||||
# - REQUEST-911-METHOD-ENFORCEMENT.conf
|
||||
# - REQUEST-912-DOS-PROTECTION.conf
|
||||
# - REQUEST-920-PROTOCOL-ENFORCEMENT.conf
|
||||
|
||||
# HTTP methods that a client is allowed to use.
|
||||
# Default: GET HEAD POST OPTIONS
|
||||
# Example: for RESTful APIs, add the following methods: PUT PATCH DELETE
|
||||
# Example: for WebDAV, add the following methods: CHECKOUT COPY DELETE LOCK
|
||||
# MERGE MKACTIVITY MKCOL MOVE PROPFIND PROPPATCH PUT UNLOCK
|
||||
# Uncomment this rule to change the default.
|
||||
#SecAction \
|
||||
# "id:900200,\
|
||||
# phase:1,\
|
||||
# nolog,\
|
||||
# pass,\
|
||||
# t:none,\
|
||||
# setvar:'tx.allowed_methods=GET HEAD POST OPTIONS'"
|
||||
|
||||
# Content-Types that a client is allowed to send in a request.
|
||||
# Default: |application/x-www-form-urlencoded| |multipart/form-data| |multipart/related|
|
||||
# |text/xml| |application/xml| |application/soap+xml| |application/x-amf| |application/json|
|
||||
# |application/cloudevents+json| |application/cloudevents-batch+json| |application/octet-stream|
|
||||
# |application/csp-report| |application/xss-auditor-report| |text/plain|
|
||||
# Uncomment this rule to change the default.
|
||||
#SecAction \
|
||||
# "id:900220,\
|
||||
# phase:1,\
|
||||
# nolog,\
|
||||
# pass,\
|
||||
# t:none,\
|
||||
# setvar:'tx.allowed_request_content_type=|application/x-www-form-urlencoded| |multipart/form-data| |multipart/related| |text/xml| |application/xml| |application/soap+xml| |application/x-amf| |application/json| |application/cloudevents+json| |application/cloudevents-batch+json| |application/octet-stream| |application/csp-report| |application/xss-auditor-report| |text/plain|'"
|
||||
|
||||
# Allowed HTTP versions.
|
||||
# Default: HTTP/1.0 HTTP/1.1 HTTP/2 HTTP/2.0
|
||||
# Example for legacy clients: HTTP/0.9 HTTP/1.0 HTTP/1.1 HTTP/2 HTTP/2.0
|
||||
# Note that some web server versions use 'HTTP/2', some 'HTTP/2.0', so
|
||||
# we include both version strings by default.
|
||||
# Uncomment this rule to change the default.
|
||||
#SecAction \
|
||||
# "id:900230,\
|
||||
# phase:1,\
|
||||
# nolog,\
|
||||
# pass,\
|
||||
# t:none,\
|
||||
# setvar:'tx.allowed_http_versions=HTTP/1.0 HTTP/1.1 HTTP/2 HTTP/2.0'"
|
||||
|
||||
# Forbidden file extensions.
|
||||
# Guards against unintended exposure of development/configuration files.
|
||||
# Default: .asa/ .asax/ .ascx/ .axd/ .backup/ .bak/ .bat/ .cdx/ .cer/ .cfg/ .cmd/ .com/ .config/ .conf/ .cs/ .csproj/ .csr/ .dat/ .db/ .dbf/ .dll/ .dos/ .htr/ .htw/ .ida/ .idc/ .idq/ .inc/ .ini/ .key/ .licx/ .lnk/ .log/ .mdb/ .old/ .pass/ .pdb/ .pol/ .printer/ .pwd/ .rdb/ .resources/ .resx/ .sql/ .swp/ .sys/ .vb/ .vbs/ .vbproj/ .vsdisco/ .webinfo/ .xsd/ .xsx/
|
||||
# Example: .bak/ .config/ .conf/ .db/ .ini/ .log/ .old/ .pass/ .pdb/ .rdb/ .sql/
|
||||
# Uncomment this rule to change the default.
|
||||
#SecAction \
|
||||
# "id:900240,\
|
||||
# phase:1,\
|
||||
# nolog,\
|
||||
# pass,\
|
||||
# t:none,\
|
||||
# setvar:'tx.restricted_extensions=.asa/ .asax/ .ascx/ .axd/ .backup/ .bak/ .bat/ .cdx/ .cer/ .cfg/ .cmd/ .com/ .config/ .conf/ .cs/ .csproj/ .csr/ .dat/ .db/ .dbf/ .dll/ .dos/ .htr/ .htw/ .ida/ .idc/ .idq/ .inc/ .ini/ .key/ .licx/ .lnk/ .log/ .mdb/ .old/ .pass/ .pdb/ .pol/ .printer/ .pwd/ .rdb/ .resources/ .resx/ .sql/ .swp/ .sys/ .vb/ .vbs/ .vbproj/ .vsdisco/ .webinfo/ .xsd/ .xsx/'"
|
||||
|
||||
# Forbidden request headers.
|
||||
# Header names should be lowercase, enclosed by /slashes/ as delimiters.
|
||||
# Blocking Proxy header prevents 'httpoxy' vulnerability: https://httpoxy.org
|
||||
# Default: /proxy/ /lock-token/ /content-range/ /if/
|
||||
# Uncomment this rule to change the default.
|
||||
#SecAction \
|
||||
# "id:900250,\
|
||||
# phase:1,\
|
||||
# nolog,\
|
||||
# pass,\
|
||||
# t:none,\
|
||||
# setvar:'tx.restricted_headers=/proxy/ /lock-token/ /content-range/ /if/'"
|
||||
|
||||
# File extensions considered static files.
|
||||
# Extensions include the dot, lowercase, enclosed by /slashes/ as delimiters.
|
||||
# Used in DoS protection rule. See section "Anti-Automation / DoS Protection".
|
||||
# Default: /.jpg/ /.jpeg/ /.png/ /.gif/ /.js/ /.css/ /.ico/ /.svg/ /.webp/
|
||||
# Uncomment this rule to change the default.
|
||||
#SecAction \
|
||||
# "id:900260,\
|
||||
# phase:1,\
|
||||
# nolog,\
|
||||
# pass,\
|
||||
# t:none,\
|
||||
# setvar:'tx.static_extensions=/.jpg/ /.jpeg/ /.png/ /.gif/ /.js/ /.css/ /.ico/ /.svg/ /.webp/'"
|
||||
|
||||
# Content-Types charsets that a client is allowed to send in a request.
|
||||
# Default: utf-8|iso-8859-1|iso-8859-15|windows-1252
|
||||
# Uncomment this rule to change the default.
|
||||
# Use "|" to separate multiple charsets like in the rule defining
|
||||
# tx.allowed_request_content_type.
|
||||
#SecAction \
|
||||
# "id:900280,\
|
||||
# phase:1,\
|
||||
# nolog,\
|
||||
# pass,\
|
||||
# t:none,\
|
||||
# setvar:'tx.allowed_request_content_type_charset=utf-8|iso-8859-1|iso-8859-15|windows-1252'"
|
||||
|
||||
#
|
||||
# -- [[ HTTP Argument/Upload Limits ]] -----------------------------------------
|
||||
#
|
||||
# Here you can define optional limits on HTTP get/post parameters and uploads.
|
||||
# This can help to prevent application specific DoS attacks.
|
||||
#
|
||||
# These values are checked in REQUEST-920-PROTOCOL-ENFORCEMENT.conf.
|
||||
# Beware of blocking legitimate traffic when enabling these limits.
|
||||
#
|
||||
|
||||
# Block request if number of arguments is too high
|
||||
# Default: unlimited
|
||||
# Example: 255
|
||||
# Uncomment this rule to set a limit.
|
||||
#SecAction \
|
||||
# "id:900300,\
|
||||
# phase:1,\
|
||||
# nolog,\
|
||||
# pass,\
|
||||
# t:none,\
|
||||
# setvar:tx.max_num_args=255"
|
||||
|
||||
# Block request if the length of any argument name is too high
|
||||
# Default: unlimited
|
||||
# Example: 100
|
||||
# Uncomment this rule to set a limit.
|
||||
#SecAction \
|
||||
# "id:900310,\
|
||||
# phase:1,\
|
||||
# nolog,\
|
||||
# pass,\
|
||||
# t:none,\
|
||||
# setvar:tx.arg_name_length=100"
|
||||
|
||||
# Block request if the length of any argument value is too high
|
||||
# Default: unlimited
|
||||
# Example: 400
|
||||
# Uncomment this rule to set a limit.
|
||||
#SecAction \
|
||||
# "id:900320,\
|
||||
# phase:1,\
|
||||
# nolog,\
|
||||
# pass,\
|
||||
# t:none,\
|
||||
# setvar:tx.arg_length=400"
|
||||
|
||||
# Block request if the total length of all combined arguments is too high
|
||||
# Default: unlimited
|
||||
# Example: 64000
|
||||
# Uncomment this rule to set a limit.
|
||||
#SecAction \
|
||||
# "id:900330,\
|
||||
# phase:1,\
|
||||
# nolog,\
|
||||
# pass,\
|
||||
# t:none,\
|
||||
# setvar:tx.total_arg_length=64000"
|
||||
|
||||
# Block request if the file size of any individual uploaded file is too high
|
||||
# Default: unlimited
|
||||
# Example: 1048576
|
||||
# Uncomment this rule to set a limit.
|
||||
#SecAction \
|
||||
# "id:900340,\
|
||||
# phase:1,\
|
||||
# nolog,\
|
||||
# pass,\
|
||||
# t:none,\
|
||||
# setvar:tx.max_file_size=1048576"
|
||||
|
||||
# Block request if the total size of all combined uploaded files is too high
|
||||
# Default: unlimited
|
||||
# Example: 1048576
|
||||
# Uncomment this rule to set a limit.
|
||||
#SecAction \
|
||||
# "id:900350,\
|
||||
# phase:1,\
|
||||
# nolog,\
|
||||
# pass,\
|
||||
# t:none,\
|
||||
# setvar:tx.combined_file_sizes=1048576"
|
||||
|
||||
|
||||
#
|
||||
# -- [[ Easing In / Sampling Percentage ]] -------------------------------------
|
||||
#
|
||||
# Adding the Core Rule Set to an existing productive site can lead to false
|
||||
# positives, unexpected performance issues and other undesired side effects.
|
||||
#
|
||||
# It can be beneficial to test the water first by enabling the CRS for a
|
||||
# limited number of requests only and then, when you have solved the issues (if
|
||||
# any) and you have confidence in the setup, to raise the ratio of requests
|
||||
# being sent into the ruleset.
|
||||
#
|
||||
# Adjust the percentage of requests that are funnelled into the Core Rules by
|
||||
# setting TX.sampling_percentage below. The default is 100, meaning that every
|
||||
# request gets checked by the CRS. The selection of requests, which are going
|
||||
# to be checked, is based on a pseudo random number generated by ModSecurity.
|
||||
#
|
||||
# If a request is allowed to pass without being checked by the CRS, there is no
|
||||
# entry in the audit log (for performance reasons), but an error log entry is
|
||||
# written. If you want to disable the error log entry, then issue the
|
||||
# following directive somewhere after the inclusion of the CRS
|
||||
# (E.g., RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf).
|
||||
#
|
||||
# SecRuleUpdateActionById 901150 "nolog"
|
||||
#
|
||||
# ATTENTION: If this TX.sampling_percentage is below 100, then some of the
|
||||
# requests will bypass the Core Rules completely and you lose the ability to
|
||||
# protect your service with ModSecurity.
|
||||
#
|
||||
# Uncomment this rule to enable this feature:
|
||||
#
|
||||
#SecAction "id:900400,\
|
||||
# phase:1,\
|
||||
# pass,\
|
||||
# nolog,\
|
||||
# setvar:tx.sampling_percentage=100"
|
||||
|
||||
|
||||
#
|
||||
# -- [[ Project Honey Pot HTTP Blacklist ]] ------------------------------------
|
||||
#
|
||||
# Optionally, you can check the client IP address against the Project Honey Pot
|
||||
# HTTPBL (dnsbl.httpbl.org). In order to use this, you need to register to get a
|
||||
# free API key. Set it here with SecHttpBlKey.
|
||||
#
|
||||
# Project Honeypot returns multiple different malicious IP types.
|
||||
# You may specify which you want to block by enabling or disabling them below.
|
||||
#
|
||||
# Ref: https://www.projecthoneypot.org/httpbl.php
|
||||
# Ref: https://github.com/SpiderLabs/ModSecurity/wiki/Reference-Manual#wiki-SecHttpBlKey
|
||||
#
|
||||
# Uncomment these rules to use this feature:
|
||||
#
|
||||
#SecHttpBlKey XXXXXXXXXXXXXXXXX
|
||||
#SecAction "id:900500,\
|
||||
# phase:1,\
|
||||
# nolog,\
|
||||
# pass,\
|
||||
# t:none,\
|
||||
# setvar:tx.block_search_ip=1,\
|
||||
# setvar:tx.block_suspicious_ip=1,\
|
||||
# setvar:tx.block_harvester_ip=1,\
|
||||
# setvar:tx.block_spammer_ip=1"
|
||||
|
||||
|
||||
#
|
||||
# -- [[ GeoIP Database ]] ------------------------------------------------------
|
||||
#
|
||||
# There are some rulesets that inspect geolocation data of the client IP address
|
||||
# (geoLookup). The CRS uses geoLookup to implement optional country blocking.
|
||||
#
|
||||
# To use geolocation, we make use of the MaxMind GeoIP database.
|
||||
# This database is not included with the CRS and must be downloaded.
|
||||
#
|
||||
# There are two formats for the GeoIP database. ModSecurity v2 uses GeoLite (.dat files),
|
||||
# and ModSecurity v3 uses GeoLite2 (.mmdb files).
|
||||
#
|
||||
# If you use ModSecurity 3, MaxMind provides a binary for updating GeoLite2 files,
|
||||
# see https://github.com/maxmind/geoipupdate.
|
||||
#
|
||||
# Download the package for your OS, and read https://dev.maxmind.com/geoip/geoipupdate/
|
||||
# for configuration options.
|
||||
#
|
||||
# Warning: GeoLite (not GeoLite2) databases are considered legacy, and not being updated anymore.
|
||||
# See https://support.maxmind.com/geolite-legacy-discontinuation-notice/ for more info.
|
||||
#
|
||||
# Therefore, if you use ModSecurity v2, you need to regenerate updated .dat files
|
||||
# from CSV files first.
|
||||
#
|
||||
# You can achieve this using https://github.com/sherpya/geolite2legacy
|
||||
# Pick the zip files from maxmind site:
|
||||
# https://geolite.maxmind.com/download/geoip/database/GeoLite2-Country-CSV.zip
|
||||
#
|
||||
# Follow the guidelines for installing the tool and run:
|
||||
# ./geolite2legacy.py -i GeoLite2-Country-CSV.zip \
|
||||
# -f geoname2fips.csv -o /usr/share/GeoliteCountry.dat
|
||||
#
|
||||
# Update the database regularly, see Step 3 of the configuration link above.
|
||||
#
|
||||
# By default, when you execute `sudo geoipupdate` on Linux, files from the free database
|
||||
# will be downloaded to `/usr/share/GeoIP` (both v1 and v2).
|
||||
#
|
||||
# Then choose from:
|
||||
# - `GeoLite2-Country.mmdb` (if you are using ModSecurity v3)
|
||||
# - `GeoLiteCountry.dat` (if you are using ModSecurity v2)
|
||||
#
|
||||
# Ref: http://blog.spiderlabs.com/2010/10/detecting-malice-with-modsecurity-geolocation-data.html
|
||||
# Ref: http://blog.spiderlabs.com/2010/11/detecting-malice-with-modsecurity-ip-forensics.html
|
||||
#
|
||||
# Uncomment only one of the next rules here to use this feature.
|
||||
# Choose the one depending on the ModSecurity version you are using, and change the path accordingly:
|
||||
#
|
||||
# For ModSecurity v3:
|
||||
#SecGeoLookupDB /usr/share/GeoIP/GeoLite2-Country.mmdb
|
||||
# For ModSecurity v2 (points to the converted one):
|
||||
#SecGeoLookupDB /usr/share/GeoIP/GeoLiteCountry.dat
|
||||
|
||||
#
|
||||
# -=[ Block Countries ]=-
|
||||
#
|
||||
# Rules in the IP Reputation file can check the client against a list of high
|
||||
# risk country codes. These countries have to be defined in the variable
|
||||
# tx.high_risk_country_codes via their ISO 3166 two-letter country code:
|
||||
# https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements
|
||||
#
|
||||
# If you are sure that you are not getting any legitimate requests from a given
|
||||
# country, then you can disable all access from that country via this variable.
|
||||
# The rule performing the test has the rule id 910100.
|
||||
#
|
||||
# This rule requires SecGeoLookupDB to be enabled and the GeoIP database to be
|
||||
# downloaded (see the section "GeoIP Database" above.)
|
||||
#
|
||||
# By default, the list is empty. A list used by some sites was the following:
|
||||
# setvar:'tx.high_risk_country_codes=UA ID YU LT EG RO BG TR RU PK MY CN'"
|
||||
#
|
||||
# Uncomment this rule to use this feature:
|
||||
#
|
||||
#SecAction \
|
||||
# "id:900600,\
|
||||
# phase:1,\
|
||||
# nolog,\
|
||||
# pass,\
|
||||
# t:none,\
|
||||
# setvar:'tx.high_risk_country_codes='"
|
||||
|
||||
|
||||
#
|
||||
# -- [[ Anti-Automation / DoS Protection ]] ------------------------------------
|
||||
#
|
||||
# Optional DoS protection against clients making requests too quickly.
|
||||
#
|
||||
# When a client is making more than 100 requests (excluding static files) within
|
||||
# 60 seconds, this is considered a 'burst'. After two bursts, the client is
|
||||
# blocked for 600 seconds.
|
||||
#
|
||||
# Requests to static files are not counted towards DoS; they are listed in the
|
||||
# 'tx.static_extensions' setting, which you can change in this file (see
|
||||
# section "HTTP Policy Settings").
|
||||
#
|
||||
# For a detailed description, see rule file REQUEST-912-DOS-PROTECTION.conf.
|
||||
#
|
||||
# Uncomment this rule to use this feature:
|
||||
#
|
||||
#SecAction \
|
||||
# "id:900700,\
|
||||
# phase:1,\
|
||||
# nolog,\
|
||||
# pass,\
|
||||
# t:none,\
|
||||
# setvar:'tx.dos_burst_time_slice=60',\
|
||||
# setvar:'tx.dos_counter_threshold=100',\
|
||||
# setvar:'tx.dos_block_timeout=600'"
|
||||
|
||||
|
||||
#
|
||||
# -- [[ Check UTF-8 encoding ]] ------------------------------------------------
|
||||
#
|
||||
# The CRS can optionally check request contents for invalid UTF-8 encoding.
|
||||
# We only want to apply this check if UTF-8 encoding is actually used by the
|
||||
# site; otherwise it will result in false positives.
|
||||
#
|
||||
# Uncomment this rule to use this feature:
|
||||
#
|
||||
#SecAction \
|
||||
# "id:900950,\
|
||||
# phase:1,\
|
||||
# nolog,\
|
||||
# pass,\
|
||||
# t:none,\
|
||||
# setvar:tx.crs_validate_utf8_encoding=1"
|
||||
|
||||
|
||||
#
|
||||
# -- [[ Blocking Based on IP Reputation ]] ------------------------------------
|
||||
#
|
||||
# Blocking based on reputation is permanent in the CRS. Unlike other rules,
|
||||
# which look at the individual request, the blocking of IPs is based on
|
||||
# a persistent record in the IP collection, which remains active for a
|
||||
# certain amount of time.
|
||||
#
|
||||
# There are two ways an individual client can become flagged for blocking:
|
||||
# - External information (RBL, GeoIP, etc.)
|
||||
# - Internal information (Core Rules)
|
||||
#
|
||||
# The record in the IP collection carries a flag, which tags requests from
|
||||
# individual clients with a flag named IP.reput_block_flag.
|
||||
# But the flag alone is not enough to have a client blocked. There is also
|
||||
# a global switch named tx.do_reput_block. This is off by default. If you set
|
||||
# it to 1 (=On), requests from clients with the IP.reput_block_flag will
|
||||
# be blocked for a certain duration.
|
||||
#
|
||||
# Variables
|
||||
# ip.reput_block_flag Blocking flag for the IP collection record
|
||||
# ip.reput_block_reason Reason (= rule message) that caused to blocking flag
|
||||
# tx.do_reput_block Switch deciding if we really block based on flag
|
||||
# tx.reput_block_duration Setting to define the duration of a block
|
||||
#
|
||||
# It may be important to know, that all the other core rules are skipped for
|
||||
# requests, when it is clear that they carry the blocking flag in question.
|
||||
#
|
||||
# Uncomment this rule to use this feature:
|
||||
#
|
||||
#SecAction \
|
||||
# "id:900960,\
|
||||
# phase:1,\
|
||||
# nolog,\
|
||||
# pass,\
|
||||
# t:none,\
|
||||
# setvar:tx.do_reput_block=1"
|
||||
#
|
||||
# Uncomment this rule to change the blocking time:
|
||||
# Default: 300 (5 minutes)
|
||||
#
|
||||
#SecAction \
|
||||
# "id:900970,\
|
||||
# phase:1,\
|
||||
# nolog,\
|
||||
# pass,\
|
||||
# t:none,\
|
||||
# setvar:tx.reput_block_duration=300"
|
||||
|
||||
|
||||
#
|
||||
# -- [[ Collection timeout ]] --------------------------------------------------
|
||||
#
|
||||
# Set the SecCollectionTimeout directive from the ModSecurity default (1 hour)
|
||||
# to a lower setting which is appropriate to most sites.
|
||||
# This increases performance by cleaning out stale collection (block) entries.
|
||||
#
|
||||
# This value should be greater than or equal to:
|
||||
# tx.reput_block_duration (see section "Blocking Based on IP Reputation") and
|
||||
# tx.dos_block_timeout (see section "Anti-Automation / DoS Protection").
|
||||
#
|
||||
# Ref: https://github.com/SpiderLabs/ModSecurity/wiki/Reference-Manual#wiki-SecCollectionTimeout
|
||||
|
||||
# Please keep this directive uncommented.
|
||||
# Default: 600 (10 minutes)
|
||||
SecCollectionTimeout 600
|
||||
|
||||
|
||||
#
|
||||
# -- [[ End of setup ]] --------------------------------------------------------
|
||||
#
|
||||
# The CRS checks the tx.crs_setup_version variable to ensure that the setup
|
||||
# has been loaded. If you are not planning to use this setup template,
|
||||
# you must manually set the tx.crs_setup_version variable before including
|
||||
# the CRS rules/* files.
|
||||
#
|
||||
# The variable is a numerical representation of the CRS version number.
|
||||
# E.g., v3.0.0 is represented as 300.
|
||||
#
|
||||
SecAction \
|
||||
"id:900990,\
|
||||
phase:1,\
|
||||
nolog,\
|
||||
pass,\
|
||||
t:none,\
|
||||
setvar:tx.crs_setup_version=332"
|
||||
8
core/modsecurity/files/coreruleset/docs/README
Normal file
8
core/modsecurity/files/coreruleset/docs/README
Normal file
@@ -0,0 +1,8 @@
|
||||
Welcome to the OWASP Core Rule Set (CRS) documentation.
|
||||
The OWASP CRS documentation is generated as a Sphinx project and is stored in a separate Github repository. While the documentation is available as part of the CRS project it is provided in the form of a git-submodule. Using a git-submodule allow us to update the documentation without making changes to the main rule repository.
|
||||
|
||||
You can download the documentation using git:
|
||||
$ git submodule init
|
||||
$ git submodule update
|
||||
|
||||
Alternatively, the latest version of the documentation is available at https://www.modsecurity.org/CRS/Documentation/
|
||||
@@ -0,0 +1,165 @@
|
||||
# ------------------------------------------------------------------------
|
||||
# OWASP ModSecurity Core Rule Set ver.3.3.2
|
||||
# Copyright (c) 2006-2020 Trustwave and contributors. All rights reserved.
|
||||
#
|
||||
# The OWASP ModSecurity Core Rule Set is distributed under
|
||||
# Apache Software License (ASL) version 2
|
||||
# Please see the enclosed LICENSE file for full details.
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
#
|
||||
# The purpose of this file is to hold LOCAL exceptions for your site. The
|
||||
# types of rules that would go into this file are one where you want to
|
||||
# short-circuit inspection and allow certain transactions to pass through
|
||||
# inspection or if you want to alter rules that are applied.
|
||||
#
|
||||
# This file is named REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf.example for a
|
||||
# very specific reason. Files affixed with the .example extension are designed
|
||||
# to contain user created/modified data. The '.example'. extension should be
|
||||
# renamed to end in .conf. The advantage of this is that when OWASP CRS is
|
||||
# updated, the updates will not overwrite a user generated configuration file.
|
||||
#
|
||||
# As a result of this design paradigm users are encouraged NOT to directly
|
||||
# modify rules. Instead they should use this
|
||||
# REQUEST-900-EXCLUSION-RULES-BEFORE-CRS and the
|
||||
# RESPONSE-999-EXCLUSION-RULES-AFTER-CRS file to modify OWASP rules using
|
||||
# methods similar to the examples specified below.
|
||||
#
|
||||
# REQUEST-900-EXCLUSION-RULES-BEFORE-CRS and
|
||||
# RESPONSE-999-EXCLUSION-RULES-AFTER-CRS serve different purposes. ModSecurity
|
||||
# effectively maintains two different context: startup, and per transaction.
|
||||
# As a rule, directives are processed within the startup context. While they
|
||||
# can affect the per transaction context they generally remain fixed during the
|
||||
# execution of ModSecurity.
|
||||
#
|
||||
# As a result if one wanted to disable a rule at bootup the SecRuleRemoveById
|
||||
# directive or one of its siblings would have to be placed AFTER the rule is
|
||||
# listed, otherwise it will not have knowledge of the rules existence (since
|
||||
# these rules are read in at the same time). This means that when using
|
||||
# directives that effect SecRules, these exceptions should be placed AFTER all
|
||||
# the existing rules. This is why RESPONSE-999-EXCLUSION-RULES-AFTER-CRS is
|
||||
# designed such that it loads LAST.
|
||||
#
|
||||
# Conversely, ModSecurity supports several actions that can change the state of
|
||||
# the underlying configuration during the per transaction context, this is when
|
||||
# rules are being processed. Generally, these are accomplished by using the
|
||||
# 'ctl' action. As these are part of a rule, they will be evaluated in the
|
||||
# order rules are applied (by physical location, considering phases). As a
|
||||
# result of this ordering a 'ctl' action should be placed with consideration to
|
||||
# when it will be executed. This is particularly relevant for the 'ctl' options
|
||||
# that involve modifying ID's (such as ruleRemoveById). In these cases it is
|
||||
# important that such rules are placed BEFORE the rule ID they will affect.
|
||||
# Unlike the setup context, by the time we process rules in the per-transaction
|
||||
# context, we are already aware of all the rule ID's. It is by this logic that
|
||||
# we include rules such as this BEFORE all the remaining rules. As a result
|
||||
# REQUEST-900-EXCLUSION-RULES-BEFORE-CRS is designed to load FIRST.
|
||||
#
|
||||
# As a general rule:
|
||||
# ctl:ruleEngine -> place in REQUEST-900-EXCLUSION-RULES-BEFORE-CRS
|
||||
# ctl:ruleRemoveById -> place in REQUEST-900-EXCLUSION-RULES-BEFORE-CRS
|
||||
# ctl:ruleRemoveByMsg -> place in REQUEST-900-EXCLUSION-RULES-BEFORE-CRS
|
||||
# ctl:ruleRemoveByTag -> place in REQUEST-900-EXCLUSION-RULES-BEFORE-CRS
|
||||
# ctl:ruleRemoveTargetById -> place in REQUEST-900-EXCLUSION-RULES-BEFORE-CRS
|
||||
# ctl:ruleRemoveTargetByMsg -> place in REQUEST-900-EXCLUSION-RULES-BEFORE-CRS
|
||||
# ctl:ruleRemoveTargetByTag -> place in REQUEST-900-EXCLUSION-RULES-BEFORE-CRS
|
||||
#
|
||||
# SecRuleRemoveById -> place in RESPONSE-999-EXCLUSION-RULES-AFTER-CRS
|
||||
# SecRuleRemoveByMsg -> place in RESPONSE-999-EXCLUSION-RULES-AFTER-CRS
|
||||
# SecRuleRemoveByTag -> place in RESPONSE-999-EXCLUSION-RULES-AFTER-CRS
|
||||
# SecRuleUpdateActionById -> place in RESPONSE-999-EXCLUSION-RULES-AFTER-CRS
|
||||
# SecRuleUpdateTargetById -> place in RESPONSE-999-EXCLUSION-RULES-AFTER-CRS
|
||||
# SecRuleUpdateTargetByMsg -> place in RESPONSE-999-EXCLUSION-RULES-AFTER-CRS
|
||||
# SecRuleUpdateTargetByTag -> place in RESPONSE-999-EXCLUSION-RULES-AFTER-CRS
|
||||
#
|
||||
#
|
||||
# What follows are a group of examples that show you how to perform rule
|
||||
# exclusions.
|
||||
#
|
||||
#
|
||||
# Example Exclusion Rule: Disable inspection for an authorized client
|
||||
#
|
||||
# This ruleset allows you to control how ModSecurity will handle traffic
|
||||
# originating from Authorized Vulnerability Scanning (AVS) sources. See
|
||||
# related blog post -
|
||||
# http://blog.spiderlabs.com/2010/12/advanced-topic-of-the-week-handling-authorized-scanning-traffic.html
|
||||
#
|
||||
# White-list ASV network block (no blocking or logging of AVS traffic) Update
|
||||
# IP network block as appropriate for your AVS traffic
|
||||
#
|
||||
# ModSec Rule Exclusion: Disable Rule Engine for known ASV IP
|
||||
# SecRule REMOTE_ADDR "@ipMatch 192.168.1.100" \
|
||||
# "id:1000,\
|
||||
# phase:1,\
|
||||
# pass,\
|
||||
# nolog,\
|
||||
# ctl:ruleEngine=Off"
|
||||
#
|
||||
#
|
||||
# Example Exclusion Rule: Removing a specific ARGS parameter from inspection
|
||||
# for an individual rule
|
||||
#
|
||||
# This rule shows how to conditionally exclude the "password"
|
||||
# parameter for rule 942100 when the REQUEST_URI is /index.php
|
||||
# ModSecurity Rule Exclusion: 942100 SQL Injection Detected via libinjection
|
||||
#
|
||||
# SecRule REQUEST_URI "@beginsWith /index.php" \
|
||||
# "id:1001,\
|
||||
# phase:1,\
|
||||
# pass,\
|
||||
# nolog,\
|
||||
# ctl:ruleRemoveTargetById=942100;ARGS:password"
|
||||
#
|
||||
#
|
||||
# Example Exclusion Rule: Removing a specific ARGS parameter from inspection
|
||||
# for only certain attacks
|
||||
#
|
||||
# Attack rules within the CRS are tagged, with tags such as 'attack-lfi',
|
||||
# 'attack-sqli', 'attack-xss', 'attack-injection-php', et cetera.
|
||||
#
|
||||
# ModSecurity Rule Exclusion: Disable inspection of ARGS:pwd
|
||||
# for all rules tagged attack-sqli
|
||||
# SecRule REQUEST_FILENAME "@endsWith /wp-login.php" \
|
||||
# "id:1002,\
|
||||
# phase:2,\
|
||||
# pass,\
|
||||
# nolog,\
|
||||
# ctl:ruleRemoveTargetByTag=attack-sqli;ARGS:pwd"
|
||||
#
|
||||
|
||||
# Example Exclusion Rule: Removing a specific ARGS parameter from inspection
|
||||
# for all CRS rules
|
||||
#
|
||||
# This rule illustrates that we can use tagging very effectively to whitelist a
|
||||
# common false positive across an entire ModSecurity instance. This can be done
|
||||
# because every rule in OWASP_CRS is tagged with OWASP_CRS. This will NOT
|
||||
# affect custom rules.
|
||||
#
|
||||
# ModSecurity Rule Exclusion: Disable inspection of ARGS:pwd
|
||||
# for all CRS rules
|
||||
# SecRule REQUEST_FILENAME "@endsWith /wp-login.php" \
|
||||
# "id:1003,\
|
||||
# phase:2,\
|
||||
# pass,\
|
||||
# nolog,\
|
||||
# ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:pwd"
|
||||
|
||||
#
|
||||
# Example Exclusion Rule: Removing a range of rules
|
||||
#
|
||||
# This rule illustrates that we can remove a rule range via a ctl action.
|
||||
# This uses the fact, that rules are grouped by topic in rule files covering
|
||||
# a certain id range.
|
||||
#
|
||||
# ModSecurity Rule Exclusion: Disable all SQLi and XSS rules
|
||||
# SecRule REQUEST_FILENAME "@beginsWith /admin" \
|
||||
# "id:1004,\
|
||||
# phase:2,\
|
||||
# pass,\
|
||||
# nolog,\
|
||||
# ctl:ruleRemoveById=941000-942999"
|
||||
#
|
||||
#
|
||||
# The application specific rule exclusion files
|
||||
# REQUEST-903.9001-DRUPAL-EXCLUSION-RULES.conf
|
||||
# REQUEST-903.9002-WORDPRESS-EXCLUSION-RULES.conf
|
||||
# bring additional examples which can be useful then tuning a service.
|
||||
@@ -0,0 +1,452 @@
|
||||
# ------------------------------------------------------------------------
|
||||
# OWASP ModSecurity Core Rule Set ver.3.3.2
|
||||
# Copyright (c) 2006-2020 Trustwave and contributors. All rights reserved.
|
||||
#
|
||||
# The OWASP ModSecurity Core Rule Set is distributed under
|
||||
# Apache Software License (ASL) version 2
|
||||
# Please see the enclosed LICENSE file for full details.
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
#
|
||||
# This file REQUEST-901-INITIALIZATION.conf initializes the Core Rules
|
||||
# and performs preparatory actions. It also fixes errors and omissions
|
||||
# of variable definitions in the file crs-setup.conf.
|
||||
# The setup.conf can and should be edited by the user, this file
|
||||
# is part of the CRS installation and should not be altered.
|
||||
#
|
||||
|
||||
|
||||
#
|
||||
# -=[ Rules Version ]=-
|
||||
#
|
||||
# Rule version data is added to the "Producer" line of Section H of the Audit log:
|
||||
#
|
||||
# - Producer: ModSecurity for Apache/2.9.1 (http://www.modsecurity.org/); OWASP_CRS/3.1.0.
|
||||
#
|
||||
# Ref: https://github.com/SpiderLabs/ModSecurity/wiki/Reference-Manual#wiki-SecComponentSignature
|
||||
#
|
||||
SecComponentSignature "OWASP_CRS/3.3.2"
|
||||
|
||||
#
|
||||
# -=[ Default setup values ]=-
|
||||
#
|
||||
# The CRS checks the tx.crs_setup_version variable to ensure that the setup
|
||||
# file is included at the correct time. This detects situations where
|
||||
# necessary settings are not defined, for instance if the file
|
||||
# inclusion order is incorrect, or if the user has forgotten to
|
||||
# include the crs-setup.conf file.
|
||||
#
|
||||
# If you are upgrading from an earlier version of the CRS and you are
|
||||
# getting this error, please make a new copy of the setup template
|
||||
# crs-setup.conf.example to crs-setup.conf, and re-apply your policy
|
||||
# changes. There have been many changes in settings syntax from CRS2
|
||||
# to CRS3, so an old setup file may cause unwanted behavior.
|
||||
#
|
||||
# If you are not planning to use the crs-setup.conf template, you must
|
||||
# manually set the tx.crs_setup_version variable before including
|
||||
# the CRS rules/* files.
|
||||
#
|
||||
# The variable is a numerical representation of the CRS version number.
|
||||
# E.g., v3.0.0 is represented as 300.
|
||||
#
|
||||
|
||||
SecRule &TX:crs_setup_version "@eq 0" \
|
||||
"id:901001,\
|
||||
phase:1,\
|
||||
deny,\
|
||||
status:500,\
|
||||
log,\
|
||||
auditlog,\
|
||||
msg:'ModSecurity Core Rule Set is deployed without configuration! Please copy the crs-setup.conf.example template to crs-setup.conf, and include the crs-setup.conf file in your webserver configuration before including the CRS rules. See the INSTALL file in the CRS directory for detailed instructions',\
|
||||
ver:'OWASP_CRS/3.3.2',\
|
||||
severity:'CRITICAL'"
|
||||
|
||||
|
||||
#
|
||||
# -=[ Default setup values ]=-
|
||||
#
|
||||
# Some constructs or individual rules will fail if certain parameters
|
||||
# are not set in the setup.conf file. The following rules will catch
|
||||
# these cases and assign sane default values.
|
||||
#
|
||||
|
||||
# Default Inbound Anomaly Threshold Level (rule 900110 in setup.conf)
|
||||
SecRule &TX:inbound_anomaly_score_threshold "@eq 0" \
|
||||
"id:901100,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ver:'OWASP_CRS/3.3.2',\
|
||||
setvar:'tx.inbound_anomaly_score_threshold=5'"
|
||||
|
||||
# Default Outbound Anomaly Threshold Level (rule 900110 in setup.conf)
|
||||
SecRule &TX:outbound_anomaly_score_threshold "@eq 0" \
|
||||
"id:901110,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ver:'OWASP_CRS/3.3.2',\
|
||||
setvar:'tx.outbound_anomaly_score_threshold=4'"
|
||||
|
||||
# Default Paranoia Level (rule 900000 in setup.conf)
|
||||
SecRule &TX:paranoia_level "@eq 0" \
|
||||
"id:901120,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ver:'OWASP_CRS/3.3.2',\
|
||||
setvar:'tx.paranoia_level=1'"
|
||||
|
||||
# Default Executing Paranoia Level (rule 900000 in setup.conf)
|
||||
SecRule &TX:executing_paranoia_level "@eq 0" \
|
||||
"id:901125,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ver:'OWASP_CRS/3.3.2',\
|
||||
setvar:'tx.executing_paranoia_level=%{TX.PARANOIA_LEVEL}'"
|
||||
|
||||
# Default Sampling Percentage (rule 900400 in setup.conf)
|
||||
SecRule &TX:sampling_percentage "@eq 0" \
|
||||
"id:901130,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ver:'OWASP_CRS/3.3.2',\
|
||||
setvar:'tx.sampling_percentage=100'"
|
||||
|
||||
# Default Anomaly Scores (rule 900100 in setup.conf)
|
||||
SecRule &TX:critical_anomaly_score "@eq 0" \
|
||||
"id:901140,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ver:'OWASP_CRS/3.3.2',\
|
||||
setvar:'tx.critical_anomaly_score=5'"
|
||||
|
||||
SecRule &TX:error_anomaly_score "@eq 0" \
|
||||
"id:901141,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ver:'OWASP_CRS/3.3.2',\
|
||||
setvar:'tx.error_anomaly_score=4'"
|
||||
|
||||
SecRule &TX:warning_anomaly_score "@eq 0" \
|
||||
"id:901142,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ver:'OWASP_CRS/3.3.2',\
|
||||
setvar:'tx.warning_anomaly_score=3'"
|
||||
|
||||
SecRule &TX:notice_anomaly_score "@eq 0" \
|
||||
"id:901143,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ver:'OWASP_CRS/3.3.2',\
|
||||
setvar:'tx.notice_anomaly_score=2'"
|
||||
|
||||
# Default do_reput_block
|
||||
SecRule &TX:do_reput_block "@eq 0" \
|
||||
"id:901150,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ver:'OWASP_CRS/3.3.2',\
|
||||
setvar:'tx.do_reput_block=0'"
|
||||
|
||||
# Default block duration
|
||||
SecRule &TX:reput_block_duration "@eq 0" \
|
||||
"id:901152,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ver:'OWASP_CRS/3.3.2',\
|
||||
setvar:'tx.reput_block_duration=300'"
|
||||
|
||||
# Default HTTP policy: allowed_methods (rule 900200)
|
||||
SecRule &TX:allowed_methods "@eq 0" \
|
||||
"id:901160,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ver:'OWASP_CRS/3.3.2',\
|
||||
setvar:'tx.allowed_methods=GET HEAD POST OPTIONS'"
|
||||
|
||||
# Default HTTP policy: allowed_request_content_type (rule 900220)
|
||||
SecRule &TX:allowed_request_content_type "@eq 0" \
|
||||
"id:901162,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ver:'OWASP_CRS/3.3.2',\
|
||||
setvar:'tx.allowed_request_content_type=|application/x-www-form-urlencoded| |multipart/form-data| |multipart/related| |text/xml| |application/xml| |application/soap+xml| |application/x-amf| |application/json| |application/cloudevents+json| |application/cloudevents-batch+json| |application/octet-stream| |application/csp-report| |application/xss-auditor-report| |text/plain|'"
|
||||
|
||||
# Default HTTP policy: allowed_request_content_type_charset (rule 900270)
|
||||
SecRule &TX:allowed_request_content_type_charset "@eq 0" \
|
||||
"id:901168,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ver:'OWASP_CRS/3.3.2',\
|
||||
setvar:'tx.allowed_request_content_type_charset=utf-8|iso-8859-1|iso-8859-15|windows-1252'"
|
||||
|
||||
# Default HTTP policy: allowed_http_versions (rule 900230)
|
||||
SecRule &TX:allowed_http_versions "@eq 0" \
|
||||
"id:901163,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ver:'OWASP_CRS/3.3.2',\
|
||||
setvar:'tx.allowed_http_versions=HTTP/1.0 HTTP/1.1 HTTP/2 HTTP/2.0'"
|
||||
|
||||
# Default HTTP policy: restricted_extensions (rule 900240)
|
||||
SecRule &TX:restricted_extensions "@eq 0" \
|
||||
"id:901164,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ver:'OWASP_CRS/3.3.2',\
|
||||
setvar:'tx.restricted_extensions=.asa/ .asax/ .ascx/ .axd/ .backup/ .bak/ .bat/ .cdx/ .cer/ .cfg/ .cmd/ .com/ .config/ .conf/ .cs/ .csproj/ .csr/ .dat/ .db/ .dbf/ .dll/ .dos/ .htr/ .htw/ .ida/ .idc/ .idq/ .inc/ .ini/ .key/ .licx/ .lnk/ .log/ .mdb/ .old/ .pass/ .pdb/ .pol/ .printer/ .pwd/ .rdb/ .resources/ .resx/ .sql/ .swp/ .sys/ .vb/ .vbs/ .vbproj/ .vsdisco/ .webinfo/ .xsd/ .xsx/'"
|
||||
|
||||
# Default HTTP policy: restricted_headers (rule 900250)
|
||||
SecRule &TX:restricted_headers "@eq 0" \
|
||||
"id:901165,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ver:'OWASP_CRS/3.3.2',\
|
||||
setvar:'tx.restricted_headers=/proxy/ /lock-token/ /content-range/ /if/'"
|
||||
|
||||
# Default HTTP policy: static_extensions (rule 900260)
|
||||
SecRule &TX:static_extensions "@eq 0" \
|
||||
"id:901166,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ver:'OWASP_CRS/3.3.2',\
|
||||
setvar:'tx.static_extensions=/.jpg/ /.jpeg/ /.png/ /.gif/ /.js/ /.css/ /.ico/ /.svg/ /.webp/'"
|
||||
|
||||
# Default enforcing of body processor URLENCODED
|
||||
SecRule &TX:enforce_bodyproc_urlencoded "@eq 0" \
|
||||
"id:901167,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ver:'OWASP_CRS/3.3.2',\
|
||||
setvar:'tx.enforce_bodyproc_urlencoded=0'"
|
||||
|
||||
#
|
||||
# -=[ Initialize internal variables ]=-
|
||||
#
|
||||
|
||||
# Initialize anomaly scoring variables.
|
||||
# All _score variables start at 0, and are incremented by the various rules
|
||||
# upon detection of a possible attack.
|
||||
# sql_error_match is used for shortcutting rules for performance reasons.
|
||||
|
||||
SecAction \
|
||||
"id:901200,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
t:none,\
|
||||
nolog,\
|
||||
ver:'OWASP_CRS/3.3.2',\
|
||||
setvar:'tx.anomaly_score=0',\
|
||||
setvar:'tx.anomaly_score_pl1=0',\
|
||||
setvar:'tx.anomaly_score_pl2=0',\
|
||||
setvar:'tx.anomaly_score_pl3=0',\
|
||||
setvar:'tx.anomaly_score_pl4=0',\
|
||||
setvar:'tx.sql_injection_score=0',\
|
||||
setvar:'tx.xss_score=0',\
|
||||
setvar:'tx.rfi_score=0',\
|
||||
setvar:'tx.lfi_score=0',\
|
||||
setvar:'tx.rce_score=0',\
|
||||
setvar:'tx.php_injection_score=0',\
|
||||
setvar:'tx.http_violation_score=0',\
|
||||
setvar:'tx.session_fixation_score=0',\
|
||||
setvar:'tx.inbound_anomaly_score=0',\
|
||||
setvar:'tx.outbound_anomaly_score=0',\
|
||||
setvar:'tx.outbound_anomaly_score_pl1=0',\
|
||||
setvar:'tx.outbound_anomaly_score_pl2=0',\
|
||||
setvar:'tx.outbound_anomaly_score_pl3=0',\
|
||||
setvar:'tx.outbound_anomaly_score_pl4=0',\
|
||||
setvar:'tx.sql_error_match=0'"
|
||||
|
||||
|
||||
#
|
||||
# -=[ Initialize collections ]=-
|
||||
#
|
||||
# Create both Global and IP collections for rules to use.
|
||||
# There are some CRS rules that assume that these two collections
|
||||
# have already been initiated.
|
||||
#
|
||||
|
||||
SecRule REQUEST_HEADERS:User-Agent "@rx ^.*$" \
|
||||
"id:901318,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
t:none,t:sha1,t:hexEncode,\
|
||||
nolog,\
|
||||
ver:'OWASP_CRS/3.3.2',\
|
||||
setvar:'tx.ua_hash=%{MATCHED_VAR}'"
|
||||
|
||||
SecAction \
|
||||
"id:901321,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
t:none,\
|
||||
nolog,\
|
||||
ver:'OWASP_CRS/3.3.2',\
|
||||
initcol:global=global,\
|
||||
initcol:ip=%{remote_addr}_%{tx.ua_hash},\
|
||||
setvar:'tx.real_ip=%{remote_addr}'"
|
||||
|
||||
#
|
||||
# -=[ Initialize Correct Body Processing ]=-
|
||||
#
|
||||
# Force request body variable and optionally request body processor
|
||||
#
|
||||
|
||||
# Force body variable
|
||||
SecRule REQBODY_PROCESSOR "!@rx (?:URLENCODED|MULTIPART|XML|JSON)" \
|
||||
"id:901340,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
nolog,\
|
||||
noauditlog,\
|
||||
msg:'Enabling body inspection',\
|
||||
tag:'paranoia-level/1',\
|
||||
ctl:forceRequestBodyVariable=On,\
|
||||
ver:'OWASP_CRS/3.3.2'"
|
||||
|
||||
# Force body processor URLENCODED
|
||||
SecRule TX:enforce_bodyproc_urlencoded "@eq 1" \
|
||||
"id:901350,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
t:none,t:urlDecodeUni,\
|
||||
nolog,\
|
||||
noauditlog,\
|
||||
msg:'Enabling forced body inspection for ASCII content',\
|
||||
ver:'OWASP_CRS/3.3.2',\
|
||||
chain"
|
||||
SecRule REQBODY_PROCESSOR "!@rx (?:URLENCODED|MULTIPART|XML|JSON)" \
|
||||
"ctl:requestBodyProcessor=URLENCODED"
|
||||
|
||||
|
||||
#
|
||||
# -=[ Easing In / Sampling Percentage ]=-
|
||||
#
|
||||
# This is used to send only a limited percentage of requests into the Core
|
||||
# Rule Set. The selection is based on TX.sampling_percentage and a pseudo
|
||||
# random number calculated below.
|
||||
#
|
||||
# Use this to ease into a new Core Rules installation with an existing
|
||||
# productive service.
|
||||
#
|
||||
# See
|
||||
# https://www.netnea.com/cms/2016/04/26/easing-in-conditional-modsecurity-rule-execution-based-on-pseudo-random-numbers/
|
||||
#
|
||||
|
||||
#
|
||||
# Generate the pseudo random number
|
||||
#
|
||||
# ATTENTION: This is no cryptographically secure random number. It's just
|
||||
# a cheap way to get some random number suitable for sampling.
|
||||
#
|
||||
# We take the entropy contained in the UNIQUE_ID. We hash that variable and
|
||||
# take the first integer numbers out of it. Theoretically, it is possible
|
||||
# there are no integers in a sha1 hash. We make sure we get two
|
||||
# integer numbers by taking the last two digits from the DURATION counter
|
||||
# (in microseconds).
|
||||
# Finally, leading zeros are removed from the two-digit random number.
|
||||
#
|
||||
|
||||
SecRule TX:sampling_percentage "@eq 100" \
|
||||
"id:901400,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ver:'OWASP_CRS/3.3.2',\
|
||||
skipAfter:END-SAMPLING"
|
||||
|
||||
SecRule UNIQUE_ID "@rx ^." \
|
||||
"id:901410,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
t:sha1,t:hexEncode,\
|
||||
nolog,\
|
||||
ver:'OWASP_CRS/3.3.2',\
|
||||
setvar:'TX.sampling_rnd100=%{MATCHED_VAR}'"
|
||||
|
||||
SecRule DURATION "@rx (..)$" \
|
||||
"id:901420,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
capture,\
|
||||
nolog,\
|
||||
ver:'OWASP_CRS/3.3.2',\
|
||||
setvar:'TX.sampling_rnd100=%{TX.sampling_rnd100}%{TX.1}'"
|
||||
|
||||
SecRule TX:sampling_rnd100 "@rx ^[a-f]*([0-9])[a-f]*([0-9])" \
|
||||
"id:901430,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
capture,\
|
||||
nolog,\
|
||||
ver:'OWASP_CRS/3.3.2',\
|
||||
setvar:'TX.sampling_rnd100=%{TX.1}%{TX.2}'"
|
||||
|
||||
SecRule TX:sampling_rnd100 "@rx ^0([0-9])" \
|
||||
"id:901440,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
capture,\
|
||||
nolog,\
|
||||
ver:'OWASP_CRS/3.3.2',\
|
||||
setvar:'TX.sampling_rnd100=%{TX.1}'"
|
||||
|
||||
|
||||
#
|
||||
# Sampling decision
|
||||
#
|
||||
# If a request is allowed to pass without being checked by the CRS, there is no
|
||||
# entry in the audit log (for performance reasons), but an error log entry is
|
||||
# being written. If you want to disable the error log entry, then issue the
|
||||
# following directive somewhere after the inclusion of the CRS
|
||||
# (E.g., RESPONSE-999-EXCEPTIONS.conf).
|
||||
#
|
||||
# SecRuleUpdateActionById 901450 "nolog"
|
||||
#
|
||||
|
||||
|
||||
SecRule TX:sampling_rnd100 "!@lt %{tx.sampling_percentage}" \
|
||||
"id:901450,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
log,\
|
||||
noauditlog,\
|
||||
msg:'Sampling: Disable the rule engine based on sampling_percentage %{TX.sampling_percentage} and random number %{TX.sampling_rnd100}',\
|
||||
ctl:ruleEngine=Off,\
|
||||
ver:'OWASP_CRS/3.3.2'"
|
||||
|
||||
SecMarker "END-SAMPLING"
|
||||
|
||||
|
||||
#
|
||||
# Configuration Plausibility Checks
|
||||
#
|
||||
|
||||
# Make sure executing paranoia level is not lower than paranoia level
|
||||
SecRule TX:executing_paranoia_level "@lt %{tx.paranoia_level}" \
|
||||
"id:901500,\
|
||||
phase:1,\
|
||||
deny,\
|
||||
status:500,\
|
||||
t:none,\
|
||||
log,\
|
||||
msg:'Executing paranoia level configured is lower than the paranoia level itself. This is illegal. Blocking request. Aborting',\
|
||||
ver:'OWASP_CRS/3.3.2'"
|
||||
@@ -0,0 +1,422 @@
|
||||
# ------------------------------------------------------------------------
|
||||
# OWASP ModSecurity Core Rule Set ver.3.3.2
|
||||
# Copyright (c) 2006-2020 Trustwave and contributors. All rights reserved.
|
||||
#
|
||||
# The OWASP ModSecurity Core Rule Set is distributed under
|
||||
# Apache Software License (ASL) version 2
|
||||
# Please see the enclosed LICENSE file for full details.
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
# These exclusions remedy false positives in a default Drupal install.
|
||||
# The exclusions are only active if crs_exclusions_drupal=1 is set.
|
||||
# See rule 900130 in crs-setup.conf.example for instructions.
|
||||
|
||||
#
|
||||
# [ POLICY ]
|
||||
#
|
||||
# Drupal is a complex application that is hard to secure with the CRS. This set
|
||||
# of exclusion rules aims to sanitise the CRS in a way that allows a default
|
||||
# Drupal setup to be installed and configured without much hassle as far as
|
||||
# ModSecurity and the CRS are concerned.
|
||||
#
|
||||
# The exclusion rules are fairly straight forward in the sense that they
|
||||
# disable CRS on a set of well-known parameter fields that are often the source
|
||||
# of false positives / false alarms of the CRS. This includes namely the
|
||||
# session cookie, the password fields and article/node bodies.
|
||||
#
|
||||
# This is based on two assumptions: - You have a basic trust in your
|
||||
# authenticated users who are allowed to edit nodes. - Drupal allows html
|
||||
# content in nodes and it protects your users from attacks via these fields.
|
||||
#
|
||||
# If you think these assumptions are wrong or if you would prefer a more
|
||||
# careful/secure approach, you can disable the exclusion rules handling of said
|
||||
# node body false positives. Do this by placing the following directive in
|
||||
# RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf.
|
||||
#
|
||||
# SecRuleRemoveById 9001200-9001299
|
||||
#
|
||||
# This will mean the CRS remain intact for the editing of node bodies.
|
||||
#
|
||||
# The exclusion rules in this file work without the need to define a Drupal
|
||||
# installation path prefix. Instead they look at the URI from the end - or
|
||||
# they use regular expressions when targeting dynamic URL. This is all not
|
||||
# totally foolproof. In some cases, an advanced attacker might be able to
|
||||
# doctor a request in a way that one of these exclusion rules is triggered
|
||||
# and the request will bypass all further inspection despite not being a
|
||||
# Drupal request at all. These exclusion rules could thus be leveraged to
|
||||
# disable the CRS completely. This is why these rules are off by default.
|
||||
#
|
||||
# The CRS rules covered by this ruleset are the rules with Paranoia Level 1 and
|
||||
# 2. If you chose to run Paranoia Level 3 or 4, you will be facing additional
|
||||
# false positives which you need to handle yourself.
|
||||
#
|
||||
# This set of exclusion rules does not cover any additional Drupal modules
|
||||
# outside of core.
|
||||
#
|
||||
# The exclusion rules are based on Drupal 8.1.10.
|
||||
#
|
||||
# And finally: This set of exclusion rules is in an experimental state. If you
|
||||
# encounter false positives with the basic Drupal functionality and they are
|
||||
# not covered by this rule file, then please report them. The aim is to be able
|
||||
# to install and run Drupal core in a seamless manner protected by
|
||||
# ModSecurity / CRS up to the paranoia level 2.
|
||||
|
||||
|
||||
SecRule &TX:crs_exclusions_drupal|TX:crs_exclusions_drupal "@eq 0" \
|
||||
"id:9001000,\
|
||||
phase:1,\
|
||||
pass,\
|
||||
t:none,\
|
||||
nolog,\
|
||||
ver:'OWASP_CRS/3.3.2',\
|
||||
skipAfter:END-DRUPAL-RULE-EXCLUSIONS"
|
||||
|
||||
SecRule &TX:crs_exclusions_drupal|TX:crs_exclusions_drupal "@eq 0" \
|
||||
"id:9001001,\
|
||||
phase:2,\
|
||||
pass,\
|
||||
t:none,\
|
||||
nolog,\
|
||||
ver:'OWASP_CRS/3.3.2',\
|
||||
skipAfter:END-DRUPAL-RULE-EXCLUSIONS"
|
||||
|
||||
|
||||
# [ Table of Contents ]
|
||||
#
|
||||
# 9001100 Session Cookie
|
||||
# 9001110 Password
|
||||
# 9001120 FREE for use
|
||||
# 9001130 FREE for use
|
||||
# 9001140 Content and Descriptions
|
||||
# 9001150 FREE for use
|
||||
# 9001160 Form Token
|
||||
# 9001170 Text Formats and Editors
|
||||
# 9001180 WYSIWYG/CKEditor Assets and Upload
|
||||
# 9001190 FREE for use
|
||||
# 9001200 Content and Descriptions
|
||||
#
|
||||
# The rule id range from 9001200 to 9001999 is reserved for future
|
||||
# use (Drupal plugins / modules).
|
||||
|
||||
|
||||
# [ Session Cookie ]
|
||||
#
|
||||
# Giving the session cookie a dynamic name is most unfortunate
|
||||
# from a ModSecurity perspective. The rule language does not allow
|
||||
# us to disable rules in a granular way for individual cookies with
|
||||
# dynamic names. So we need to disable rule causing false positives
|
||||
# for all cookies and their names.
|
||||
#
|
||||
# Rule Exclusion Session Cookie: 942450 SQL Hex Encoding Identified
|
||||
#
|
||||
SecAction "id:9001100,\
|
||||
phase:2,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ctl:ruleRemoveTargetById=942450;REQUEST_COOKIES_NAMES,\
|
||||
ctl:ruleRemoveTargetById=942450;REQUEST_COOKIES,\
|
||||
ver:'OWASP_CRS/3.3.2'"
|
||||
|
||||
|
||||
#
|
||||
# [ Password ]
|
||||
#
|
||||
# Disable the CRS completely for all occurrences of passwords.
|
||||
#
|
||||
SecRule REQUEST_FILENAME "@endsWith /core/install.php" \
|
||||
"id:9001110,\
|
||||
phase:2,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:account[pass][pass1],\
|
||||
ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:account[pass][pass2],\
|
||||
ver:'OWASP_CRS/3.3.2'"
|
||||
|
||||
SecRule REQUEST_FILENAME "@endsWith /user/login" \
|
||||
"id:9001112,\
|
||||
phase:2,\
|
||||
pass,\
|
||||
t:none,\
|
||||
nolog,\
|
||||
ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:pass,\
|
||||
ver:'OWASP_CRS/3.3.2'"
|
||||
|
||||
SecRule REQUEST_FILENAME "@endsWith /admin/people/create" \
|
||||
"id:9001114,\
|
||||
phase:2,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:pass[pass1],\
|
||||
ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:pass[pass2],\
|
||||
ver:'OWASP_CRS/3.3.2'"
|
||||
|
||||
SecRule REQUEST_FILENAME "@rx /user/[0-9]+/edit$" \
|
||||
"id:9001116,\
|
||||
phase:2,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:current_pass,\
|
||||
ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:pass[pass1],\
|
||||
ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:pass[pass2],\
|
||||
ver:'OWASP_CRS/3.3.2'"
|
||||
|
||||
|
||||
#
|
||||
# [ Admin Settings (general) ]
|
||||
#
|
||||
# Disable known false positives for various fields used on admin pages.
|
||||
#
|
||||
# Rule Exclusion: 920271 Invalid character in request on multiple fields/paths
|
||||
# Rule Exclusion: 942430 Restricted SQL Character Anomaly Detection (args)
|
||||
# Disabled completely for admin/config pages
|
||||
# For the people/accounts page, we disable the CRS completely for a number of
|
||||
# freeform text fields.
|
||||
#
|
||||
SecRule REQUEST_FILENAME "@contains /admin/config/" \
|
||||
"id:9001122,\
|
||||
phase:2,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ctl:ruleRemoveById=942430,\
|
||||
ver:'OWASP_CRS/3.3.2'"
|
||||
|
||||
SecRule REQUEST_FILENAME "@endsWith /admin/config/people/accounts" \
|
||||
"id:9001124,\
|
||||
phase:2,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ctl:ruleRemoveById=920271,\
|
||||
ctl:ruleRemoveById=942440,\
|
||||
ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:user_mail_cancel_confirm_body,\
|
||||
ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:user_mail_password_reset_body,\
|
||||
ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:user_mail_register_admin_created_body,\
|
||||
ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:user_mail_register_no_approval_required_body,\
|
||||
ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:user_mail_register_pending_approval_body,\
|
||||
ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:user_mail_status_activated_body,\
|
||||
ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:user_mail_status_blocked_body,\
|
||||
ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:user_mail_status_canceled_body,\
|
||||
ver:'OWASP_CRS/3.3.2'"
|
||||
|
||||
SecRule REQUEST_FILENAME "@endsWith /admin/config/development/configuration/single/import" \
|
||||
"id:9001126,\
|
||||
phase:2,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ctl:ruleRemoveById=920271,\
|
||||
ctl:ruleRemoveById=942440,\
|
||||
ver:'OWASP_CRS/3.3.2'"
|
||||
|
||||
SecRule REQUEST_FILENAME "@endsWith /admin/config/development/maintenance" \
|
||||
"id:9001128,\
|
||||
phase:2,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ctl:ruleRemoveById=942440,\
|
||||
ver:'OWASP_CRS/3.3.2'"
|
||||
|
||||
|
||||
#
|
||||
#
|
||||
# [ Content and Descriptions ]
|
||||
#
|
||||
# Disable known false positives for field "ids[]".
|
||||
#
|
||||
# Rule Exclusion: 942130 SQL Injection Attack: SQL Tautology Detected
|
||||
#
|
||||
SecRule REQUEST_FILENAME "@endsWith /contextual/render" \
|
||||
"id:9001140,\
|
||||
phase:2,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ctl:ruleRemoveTargetById=942130;ARGS:ids[],\
|
||||
ver:'OWASP_CRS/3.3.2'"
|
||||
|
||||
|
||||
#
|
||||
# [ Form Token / Build ID ]
|
||||
#
|
||||
# Rule Exclusion for form_build_id: 942440 SQL Comment Sequence Detected on ...
|
||||
# Rule Exclusion for form_token: 942450 SQL Hex Encoding
|
||||
# Rule Exclusion for form_build_id: 942450 SQL Hex Encoding
|
||||
#
|
||||
# This is applied site-wide.
|
||||
#
|
||||
SecAction "id:9001160,\
|
||||
phase:2,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ctl:ruleRemoveTargetById=942440;ARGS:form_build_id,\
|
||||
ctl:ruleRemoveTargetById=942450;ARGS:form_token,\
|
||||
ctl:ruleRemoveTargetById=942450;ARGS:form_build_id,\
|
||||
ver:'OWASP_CRS/3.3.2'"
|
||||
|
||||
|
||||
#
|
||||
# [ Text Formats and Editors ]
|
||||
#
|
||||
# Disable the CRS completely for two fields triggering many, many rules
|
||||
#
|
||||
# Rule Exclusion for two fields: 942440 SQL Comment Sequence Detected
|
||||
#
|
||||
SecRule REQUEST_FILENAME "@endsWith /admin/config/content/formats/manage/full_html" \
|
||||
"id:9001170,\
|
||||
phase:2,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:editor[settings][toolbar][button_groups],\
|
||||
ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:filters[filter_html][settings][allowed_html],\
|
||||
ver:'OWASP_CRS/3.3.2'"
|
||||
|
||||
|
||||
#
|
||||
# [ WYSIWYG/CKEditor Assets and Upload ]
|
||||
#
|
||||
# Disable the unnecessary requestBodyAccess and for binary uploads
|
||||
# bigger than an arbitrary limit of 31486341 bytes.
|
||||
#
|
||||
# Extensive checks make sure these uploads are really legitimate.
|
||||
#
|
||||
# Rule 9001180 was commented out in 2021 in order to fight CVE-2021-35368.
|
||||
#
|
||||
#SecRule REQUEST_METHOD "@streq POST" \
|
||||
# "id:9001180,\
|
||||
# phase:1,\
|
||||
# pass,\
|
||||
# t:none,\
|
||||
# nolog,\
|
||||
# noauditlog,\
|
||||
# ver:'OWASP_CRS/3.3.0',\
|
||||
# chain"
|
||||
# SecRule REQUEST_FILENAME "@rx /admin/content/assets/add/[a-z]+$" \
|
||||
# "chain"
|
||||
# SecRule REQUEST_COOKIES:/S?SESS[a-f0-9]+/ "@rx ^[a-zA-Z0-9_-]+" \
|
||||
# "ctl:requestBodyAccess=Off"
|
||||
|
||||
# Rule 9001182 was commented out in 2021 in order to fight CVE-2021-35368.
|
||||
#
|
||||
#SecRule REQUEST_METHOD "@streq POST" \
|
||||
# "id:9001182,\
|
||||
# phase:1,\
|
||||
# pass,\
|
||||
# t:none,\
|
||||
# nolog,\
|
||||
# noauditlog,\
|
||||
# ver:'OWASP_CRS/3.3.0',\
|
||||
# chain"
|
||||
# SecRule REQUEST_FILENAME "@rx /admin/content/assets/manage/[0-9]+$" \
|
||||
# "chain"
|
||||
# SecRule ARGS:destination "@streq admin/content/assets" \
|
||||
# "chain"
|
||||
# SecRule REQUEST_HEADERS:Content-Length "@gt 31486341" \
|
||||
# "chain"
|
||||
# SecRule REQUEST_COOKIES:/S?SESS[a-f0-9]+/ "@rx ^[a-zA-Z0-9_-]+" \
|
||||
# "ctl:requestBodyAccess=Off"
|
||||
|
||||
# Rule 9001184 was commented out in 2021 in order to fight CVE-2021-35368.
|
||||
#
|
||||
#SecRule REQUEST_METHOD "@streq POST" \
|
||||
# "id:9001184,\
|
||||
# phase:1,\
|
||||
# pass,\
|
||||
# t:none,\
|
||||
# nolog,\
|
||||
# noauditlog,\
|
||||
# ver:'OWASP_CRS/3.3.0',\
|
||||
# chain"
|
||||
# SecRule REQUEST_FILENAME "@rx /file/ajax/field_asset_[a-z0-9_]+/[ua]nd/0/form-[a-z0-9A-Z_-]+$" \
|
||||
# "chain"
|
||||
# SecRule REQUEST_HEADERS:Content-Length "@gt 31486341" \
|
||||
# "chain"
|
||||
# SecRule REQUEST_HEADERS:Content-Type "@rx ^(?i)multipart/form-data" \
|
||||
# "chain"
|
||||
# SecRule REQUEST_COOKIES:/S?SESS[a-f0-9]+/ "@rx ^[a-zA-Z0-9_-]+" \
|
||||
# "ctl:requestBodyAccess=Off"
|
||||
|
||||
|
||||
#
|
||||
# [ Content and Descriptions ]
|
||||
#
|
||||
# Disable the CRS completely for node bodies and other free text fields.
|
||||
# Other rules are disabled individually.
|
||||
#
|
||||
# Rule Exclusion for ARGS:uid[0][target_id]: 942410 SQL Injection Attack
|
||||
# Rule Exclusion for ARGS:destination: 932110 RCE: Windows Command Inj.
|
||||
#
|
||||
SecRule REQUEST_FILENAME "@endsWith /node/add/article" \
|
||||
"id:9001200,\
|
||||
phase:2,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:body[0][value],\
|
||||
ctl:ruleRemoveTargetById=942410;ARGS:uid[0][target_id],\
|
||||
ver:'OWASP_CRS/3.3.2'"
|
||||
|
||||
SecRule REQUEST_FILENAME "@endsWith /node/add/page" \
|
||||
"id:9001202,\
|
||||
phase:2,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:body[0][value],\
|
||||
ctl:ruleRemoveTargetById=942410;ARGS:uid[0][target_id],\
|
||||
ver:'OWASP_CRS/3.3.2'"
|
||||
|
||||
SecRule REQUEST_FILENAME "@rx /node/[0-9]+/edit$" \
|
||||
"id:9001204,\
|
||||
phase:2,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:body[0][value],\
|
||||
ctl:ruleRemoveTargetById=942410;ARGS:uid[0][target_id],\
|
||||
ctl:ruleRemoveTargetById=932110;ARGS:destination,\
|
||||
ver:'OWASP_CRS/3.3.2'"
|
||||
|
||||
SecRule REQUEST_FILENAME "@endsWith /block/add" \
|
||||
"id:9001206,\
|
||||
phase:2,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:body[0][value],\
|
||||
ver:'OWASP_CRS/3.3.2'"
|
||||
|
||||
SecRule REQUEST_FILENAME "@endsWith /admin/structure/block/block-content/manage/basic" \
|
||||
"id:9001208,\
|
||||
phase:2,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:description,\
|
||||
ver:'OWASP_CRS/3.3.2'"
|
||||
|
||||
SecRule REQUEST_FILENAME "@rx /editor/filter_xss/(?:full|basic)_html$" \
|
||||
"id:9001210,\
|
||||
phase:2,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:value,\
|
||||
ver:'OWASP_CRS/3.3.2'"
|
||||
|
||||
SecRule REQUEST_FILENAME "@rx /user/[0-9]+/contact$" \
|
||||
"id:9001212,\
|
||||
phase:2,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:message[0][value],\
|
||||
ver:'OWASP_CRS/3.3.2'"
|
||||
|
||||
SecRule REQUEST_FILENAME "@endsWith /admin/config/development/maintenance" \
|
||||
"id:9001214,\
|
||||
phase:2,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:maintenance_mode_message,\
|
||||
ver:'OWASP_CRS/3.3.2'"
|
||||
|
||||
SecRule REQUEST_FILENAME "@endsWith /admin/config/services/rss-publishing" \
|
||||
"id:9001216,\
|
||||
phase:2,\
|
||||
pass,\
|
||||
nolog,\
|
||||
ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:feed_description,\
|
||||
ver:'OWASP_CRS/3.3.2'"
|
||||
|
||||
|
||||
SecMarker "END-DRUPAL-RULE-EXCLUSIONS"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user