Skip to content

Commit

Permalink
feat: Add security.txt support
Browse files Browse the repository at this point in the history
This commit adds support for the security.txt file, including the necessary configuration files and templates. The security.txt file provides information about security policies and contact details for reporting vulnerabilities.
  • Loading branch information
TheophileDiot committed Jul 22, 2024
1 parent 3540903 commit 33ec069
Show file tree
Hide file tree
Showing 5 changed files with 329 additions and 1 deletion.
3 changes: 2 additions & 1 deletion src/common/core/order.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"blacklist",
"greylist",
"bunkernet",
"limit"
"limit",
"securitytxt"
],
"init_worker": [
"redis",
Expand Down
38 changes: 38 additions & 0 deletions src/common/core/securitytxt/confs/server-http/securitytxt.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{% if USE_SECURITYTXT == "yes" and SECURITYTXT_CONTACT != "" +%}
location = {{ SECURITYTXT_URI }} {
default_type 'text/plain; charset=utf-8';
root /usr/share/bunkerweb/core/securitytxt/templates;
content_by_lua_block {
local logger = require "bunkerweb.logger":new("SECURITYTXT")
local helpers = require "bunkerweb.helpers"

local ngx = ngx
local ERR = ngx.ERR
local INFO = ngx.INFO
local fill_ctx = helpers.fill_ctx
local save_ctx = helpers.save_ctx
local tostring = tostring

local ok, ret, errors, ctx = fill_ctx()
if not ok then
logger:log(ERR, "fill_ctx() failed : " .. ret)
elseif errors then
for i, error in ipairs(errors) do
logger:log(ERR, "fill_ctx() error " .. tostring(i) .. " : " .. error)
end
end
local securitytxt = require "securitytxt.securitytxt":new(ctx)
local ret = securitytxt:content()
if not ret.ret then
logger:log(ERR, "securitytxt:content() failed : " .. ret.msg)
else
logger:log(INFO, "securitytxt:content() success : " .. ret.msg)
end
save_ctx(ctx)
}
}

location = /security.txt {
return 301 {{ SECURITYTXT_URI }};
}
{% endif %}
115 changes: 115 additions & 0 deletions src/common/core/securitytxt/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
{
"id": "securitytxt",
"name": "Security.txt",
"description": "Manage the security.txt file. A proposed standard which allows websites to define security policies.",
"version": "1.0",
"stream": "yes",
"settings": {
"USE_SECURITYTXT": {
"context": "multisite",
"default": "no",
"help": "Enable security.txt file.",
"id": "use-securitytxt",
"label": "Use security.txt",
"regex": "^(yes|no)$",
"type": "check"
},
"SECURITYTXT_URI": {
"context": "multisite",
"default": "/.well-known/security.txt",
"help": "Indicates the URI where the \"security.txt\" file will be accessible from.",
"id": "securitytxt-uri",
"label": "Security.txt URI",
"regex": "^\\/\\S*$",
"type": "text"
},
"SECURITYTXT_CONTACT": {
"context": "multisite",
"default": "",
"help": "Indicates a method that researchers should use for reporting security vulnerabilities such as an email address, a phone number, and/or a web page with contact information. (If the value is empty, the security.txt file will not be created as it is a required field)",
"id": "securitytxt-contact",
"label": "Security.txt contact",
"regex": "^((mailto:|tel:|https:\\/\\/)\\S+)?$",
"type": "text",
"multiple": "securitytxt-contact"
},
"SECURITYTXT_EXPIRES": {
"context": "multisite",
"default": "",
"help": "Indicates the date and time after which the data contained in the \"security.txt\" file is considered stale and should not be used (If the value is empty, the value will always be the current date and time + 1 year).",
"id": "securitytxt-expiration",
"label": "Security.txt expiration",
"regex": "^([0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(Z|[+-]([01][0-9]|2[0-3]):[0-5][0-9]))?$",
"type": "text"
},
"SECURITYTXT_ENCRYPTION": {
"context": "multisite",
"default": "",
"help": "Indicates an encryption key that security researchers should use for encrypted communication.",
"id": "securitytxt-encryption",
"label": "Security.txt encryption",
"regex": "^((https:\\/\\/|dns:|openpgp4fpr:)\\S+)?$",
"type": "text",
"multiple": "securitytxt-encryption"
},
"SECURITYTXT_ACKNOWLEDGEMENTS": {
"context": "multisite",
"default": "",
"help": "Indicates a link to a page where security researchers are recognized for their reports.",
"id": "securitytxt-acknowledgement",
"label": "Security.txt acknowledgement",
"regex": "^(https:\\/\\/\\S+)?$",
"type": "text",
"multiple": "securitytxt-acknowledgement"
},
"SECURITYTXT_PREFERRED_LANG": {
"context": "multisite",
"default": "en",
"help": "Can be used to indicate a set of natural languages that are preferred when submitting security reports.",
"id": "securitytxt-preferred-lang",
"label": "Security.txt preferred language",
"regex": "^[a-zA-Z]{2,3}(-[a-zA-Z]{2})?(, [a-zA-Z]{2,3}(-[a-zA-Z]{2})?)*$",
"type": "text"
},
"SECURITYTXT_CANONICAL": {
"context": "multisite",
"default": "",
"help": "Indicates the canonical URIs where the \"security.txt\" file is located, which is usually something like \"https://example.com/.well-known/security.txt\". (If the value is empty, the default value will be automatically generated from the site URL + SECURITYTXT_URI)",
"id": "securitytxt-canonical",
"label": "Security.txt canonical",
"regex": "^(https:\\/\\/\\S+)?$",
"type": "text",
"multiple": "securitytxt-canonical"
},
"SECURITYTXT_POLICY": {
"context": "multisite",
"default": "",
"help": "Indicates a link to where the vulnerability disclosure policy is located.",
"id": "securitytxt-policy",
"label": "Security.txt policy",
"regex": "^(https:\\/\\/\\S+)?$",
"type": "text",
"multiple": "securitytxt-policy"
},
"SECURITYTXT_HIRING": {
"context": "multisite",
"default": "",
"help": "Used for linking to the vendor's security-related job positions.",
"id": "securitytxt-hiring",
"label": "Security.txt hiring",
"regex": "^(https:\\/\\/\\S+)?$",
"type": "text",
"multiple": "securitytxt-hiring"
},
"SECURITYTXT_CSAF": {
"context": "multisite",
"default": "",
"help": "A link to the provider-metadata.json of your CSAF (Common Security Advisory Framework) provider.",
"id": "securitytxt-csaf",
"label": "Security.txt CSAF",
"regex": "^(https:\\/\\/\\S+)?$",
"type": "text",
"multiple": "securitytxt-csaf"
}
}
}
147 changes: 147 additions & 0 deletions src/common/core/securitytxt/securitytxt.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
local class = require "middleclass"
local plugin = require "bunkerweb.plugin"
local utils = require "bunkerweb.utils"

local ngx = ngx
local ERR = ngx.ERR
local get_phase = ngx.get_phase
local subsystem = ngx.config.subsystem
local get_multiple_variables = utils.get_multiple_variables

local template
local render = nil
if subsystem == "http" then
template = require "resty.template"
render = template.render
end

local securitytxt = class("securitytxt", plugin)

function securitytxt:initialize(ctx)
-- Call parent initialize
plugin.initialize(self, "securitytxt", ctx)
-- Load data from datastore if needed
if get_phase() ~= "init" then
-- Get security policies from datastore
local security_policies, err = self.datastore:get("plugin_securitytxt_security_policies", true)
if not security_policies then
self.logger:log(ERR, err)
return
end
self.security_policies = {
["uri"] = "",
["acknowledgements"] = {},
["canonical"] = {},
["contact"] = {},
["encryption"] = {},
["expires"] = "",
["hiring"] = {},
["policy"] = {},
["preferred_lang"] = "",
["csaf"] = {},
}
-- Extract global security policies
if security_policies.global then
for k, v in pairs(security_policies.global) do
self.security_policies[k] = v
end
end
-- Extract and overwrite if needed server security policies
if security_policies[self.ctx.bw.server_name] then
for k, v in pairs(security_policies[self.ctx.bw.server_name]) do
self.security_policies[k] = v
end
end
end
end

function securitytxt:init()
-- Get variables
local variables, err = get_multiple_variables({
"SECURITYTXT_URI",
"SECURITYTXT_ACKNOWLEDGEMENTS",
"SECURITYTXT_CANONICAL",
"SECURITYTXT_CONTACT",
"SECURITYTXT_ENCRYPTION",
"SECURITYTXT_EXPIRES",
"SECURITYTXT_HIRING",
"SECURITYTXT_POLICY",
"SECURITYTXT_PREFERRED_LANG",
"SECURITYTXT_CSAF",
})
if variables == nil then
return self:ret(false, err)
end
-- Store security policies name and value
local data = {}
local key
for srv, vars in pairs(variables) do
if data[srv] == nil then
data[srv] = {
["uri"] = "",
["acknowledgements"] = {},
["canonical"] = {},
["contact"] = {},
["encryption"] = {},
["expires"] = "",
["hiring"] = {},
["policy"] = {},
["preferred_lang"] = "",
["csaf"] = {},
}
end
for var, value in pairs(vars) do
if value ~= "" then
key = string.lower(string.gsub(string.gsub(var, "^SECURITYTXT_", ""), "_%d+$", ""))
if key == "uri" or key == "expires" or key == "preferred_lang" then
data[srv][key] = value
else
data[srv][key][#data[srv][key] + 1] = value
end
end
end
end
local ok
ok, err = self.datastore:set("plugin_securitytxt_security_policies", data, nil, true)
if not ok then
return self:ret(false, err)
end
return self:ret(true, "successfully loaded security policies")
end

function securitytxt:content()
-- Check if content is needed
if self.variables["USE_SECURITYTXT"] == "no" then
return self:ret(true, "securitytxt not activated")
end

-- Check if display content is needed
if self.ctx.bw.uri ~= self.variables["SECURITYTXT_URI"] then
return self:ret(true, "securitytxt not requested")
end

-- Check if a contact key is set
if self.security_policies["contact"] == nil or #self.security_policies["contact"] == 0 then
return self:ret(false, "no contact key set")
end

local data = {
server_name = self.ctx.bw.server_name,
scheme = self.ctx.bw.scheme,
}

for k, v in pairs(self.security_policies) do
data[k] = v
end

-- If expires isn't set, set it to 1 year in the future and make it a ISO.8601-1 and ISO.8601-2 date
if data["expires"] == "" then
data["expires"] = os.date("%Y-%m-%dT%H:%M:%S", os.time() + 31536000)
end

-- Render content
render("security.txt", data)
return self:ret(true, "content displayed")
end

return securitytxt
27 changes: 27 additions & 0 deletions src/common/core/securitytxt/templates/security.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{% for _, c in ipairs(contact) do %}
Contact: {*c*}
{% end %}
Expires: {*expires*}
{% for _, e in ipairs(encryption) do %}
Encryption: {*e*}
{% end %}
{% for _, a in ipairs(acknowledgements) do %}
Acknowledgements: {*a*}
{% end %}
Preferred-Languages: {*preferred_lang*}
{% if canonical and #canonical > 0 then %}
{% for _, ca in ipairs(canonical) do %}
Canonical: {*ca*}
{% end %}
{% else %}
Canonical: {*scheme*}://{*server_name*}{*uri*}
{% end %}
{% for _, p in ipairs(policy) do %}
Policy: {*p*}
{% end %}
{% for _, h in ipairs(hiring) do %}
Hiring: {*h*}
{% end %}
{% for _, cs in ipairs(csaf) do %}
CSAF: {*cs*}
{% end %}

0 comments on commit 33ec069

Please sign in to comment.