Skip to content

Commit

Permalink
Merge pull request bunkerity#1282 from bunkerity/dev
Browse files Browse the repository at this point in the history
Merge branch "dev" into branch "staging"
  • Loading branch information
TheophileDiot authored Jun 17, 2024
2 parents f7280ce + 2c3fe6b commit 18517b9
Show file tree
Hide file tree
Showing 11 changed files with 183 additions and 115 deletions.
2 changes: 1 addition & 1 deletion docs/security-tuning.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ Here is the list of related settings :
| --------------------------- | ------------ | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `USE_ANTIBOT` | `no` | multisite | no | Activate antibot feature. |
| `ANTIBOT_URI` | `/challenge` | multisite | no | Unused URI that clients will be redirected to to solve the challenge. |
| `ANTIBOT_RECAPTCHA_SCORE` | `0.7` | multisite | no | Minimum score required for reCAPTCHA challenge. |
| `ANTIBOT_RECAPTCHA_SCORE` | `0.7` | multisite | no | Minimum score required for reCAPTCHA challenge (Only compatible with reCAPTCHA v3). |
| `ANTIBOT_RECAPTCHA_SITEKEY` | | multisite | no | Sitekey for reCAPTCHA challenge. |
| `ANTIBOT_RECAPTCHA_SECRET` | | multisite | no | Secret for reCAPTCHA challenge. |
| `ANTIBOT_HCAPTCHA_SITEKEY` | | multisite | no | Sitekey for hCaptcha challenge. |
Expand Down
2 changes: 1 addition & 1 deletion docs/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Bot detection by using a challenge.
|`ANTIBOT_URI` |`/challenge`|multisite|no |Unused URI that clients will be redirected to to solve the challenge. |
|`ANTIBOT_TIME_RESOLVE` |`60` |multisite|no |Maximum time (in seconds) clients have to resolve the challenge. Once this time has passed, a new challenge will be generated.|
|`ANTIBOT_TIME_VALID` |`86400` |multisite|no |Maximum validity time of solved challenges. Once this time has passed, clients will need to resolve a new one. |
|`ANTIBOT_RECAPTCHA_SCORE` |`0.7` |multisite|no |Minimum score required for reCAPTCHA challenge. |
|`ANTIBOT_RECAPTCHA_SCORE` |`0.7` |multisite|no |Minimum score required for reCAPTCHA challenge (Only compatible with reCAPTCHA v3). |
|`ANTIBOT_RECAPTCHA_SITEKEY`| |multisite|no |Sitekey for reCAPTCHA challenge. |
|`ANTIBOT_RECAPTCHA_SECRET` | |multisite|no |Secret for reCAPTCHA challenge. |
|`ANTIBOT_HCAPTCHA_SITEKEY` | |multisite|no |Sitekey for hCaptcha challenge. |
Expand Down
11 changes: 9 additions & 2 deletions src/autoconf/Config.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,12 @@ def __get_full_env(self) -> dict:
if not server_name:
continue
for variable, value in service.items():
if self._db.is_setting(variable, multisite=True):
config[f"{server_name}_{variable}"] = value
if variable.startswith("CUSTOM_CONF") or not variable.isupper():
continue
if not self._db.is_setting(variable, multisite=True):
self.__logger.warning(f"Variable {variable}: {value} is not a valid multisite setting, ignoring it")
continue
config[f"{server_name}_{variable}"] = value
config["SERVER_NAME"] += f" {server_name}"
config["SERVER_NAME"] = config["SERVER_NAME"].strip()
return config
Expand Down Expand Up @@ -134,13 +138,15 @@ def apply(

# update instances in database
if "instances" in changes:
self.__logger.debug(f"Updating instances in database: {self.__instances}")
err = self._db.update_instances(self.__instances, changed=False)
if err:
self.__logger.error(f"Failed to update instances: {err}")

# save config to database
changed_plugins = []
if "config" in changes:
self.__logger.debug(f"Saving config in database: {self.__config}")
err = self._db.save_config(self.__config, "autoconf", changed=False)
if isinstance(err, str):
success = False
Expand All @@ -149,6 +155,7 @@ def apply(

# save custom configs to database
if "custom_configs" in changes:
self.__logger.debug(f"Saving custom configs in database: {custom_configs}")
err = self._db.save_custom_configs(custom_configs, "autoconf", changed=False)
if err:
success = False
Expand Down
9 changes: 9 additions & 0 deletions src/bw/loading/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,15 @@
</div>
<!-- text -->

{-raw-}
<script nonce="{{ nonce_script }}">
// Automatically refresh the page after 2 seconds
setTimeout(() => {
location.reload();
}, 2000);
</script>
{-raw-}

<footer class="fixed bottom-1.5 lg:bottom-2">
<div class="flex justify-center pb-2">
<img
Expand Down
10 changes: 7 additions & 3 deletions src/common/confs/default-server-http.conf
Original file line number Diff line number Diff line change
Expand Up @@ -67,18 +67,22 @@ server {
end

local nonce_style = rand(16)
local nonce_script = rand(16)

-- Override CSP header
ngx.header["Content-Security-Policy"] = "default-src 'none'; form-action 'self'; img-src 'self' data:; style-src 'self' 'nonce-"
ngx.header["Content-Security-Policy"] = "default-src 'none'; script-src http: https: 'unsafe-inline' 'strict-dynamic' 'nonce-"
.. nonce_script
.. "'; style-src 'nonce-"
.. nonce_style
.. "'; font-src 'self' data:; base-uri 'self'; require-trusted-types-for 'script';"
.. "'; base-uri 'none'; img-src 'self' data:; font-src 'self' data:; require-trusted-types-for 'script';"

-- Remove server header
ngx.header["Server"] = nil

-- Render template
render("index.html", {
nonce_style = nonce_style
nonce_style = nonce_style,
nonce_script = nonce_script
})
}
}
Expand Down
72 changes: 35 additions & 37 deletions src/common/core/antibot/antibot.lua
Original file line number Diff line number Diff line change
Expand Up @@ -60,42 +60,36 @@ function antibot:header()
return self:ret(true, "client already resolved the challenge", nil, self.session_data.original_uri)
end

-- Override headers
local header = "Content-Security-Policy"
if self.variables["CONTENT_SECURITY_POLICY_REPORT_ONLY"] == "yes" then
header = header .. "-Report-Only"
end
-- Override CSP header
local csp_directives = {
["default-src"] = "'none'",
["base-uri"] = "'none'",
["img-src"] = "'self' data:",
["font-src"] = "'self' data:",
["script-src"] = "http: https: 'unsafe-inline' 'strict-dynamic' 'nonce-"
.. self.ctx.bw.antibot_nonce_script
.. "'",
["style-src"] = "'self' 'nonce-" .. self.ctx.bw.antibot_nonce_style .. "'",
["require-trusted-types-for"] = "'script'",
}
if self.session_data.type == "recaptcha" then
ngx.header[header] = "default-src 'none'; form-action 'self'; script-src 'strict-dynamic' 'nonce-"
.. self.session_data.nonce_script
.. "' https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ 'unsafe-inline' http: https:;"
.. " img-src https://www.gstatic.com/recaptcha/ 'self' data:; "
.. " frame-src https://www.google.com/recaptcha/ https://recaptcha.google.com/recaptcha/;"
.. " style-src 'self' 'nonce-"
.. self.session_data.nonce_style
.. "'; font-src 'self' https://fonts.gstatic.com data:; base-uri 'self';"
csp_directives["script-src"] = csp_directives["script-src"]
.. " https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/"
csp_directives["frame-src"] = "https://www.google.com/recaptcha/ https://recaptcha.google.com/recaptcha/"
elseif self.session_data.type == "hcaptcha" then
ngx.header[header] = "default-src 'none'; form-action 'self'; script-src 'strict-dynamic' 'nonce-"
.. self.session_data.nonce_script
.. "' https://hcaptcha.com https://*.hcaptcha.com 'unsafe-inline' http: https:; img-src 'self' data:;"
.. " frame-src https://hcaptcha.com https://*.hcaptcha.com; style-src 'self' 'nonce-"
.. self.session_data.nonce_style
.. "' https://hcaptcha.com https://*.hcaptcha.com; connect-src https://hcaptcha.com https://*.hcaptcha.com; "
.. " font-src 'self' data:; base-uri 'self';"
csp_directives["script-src"] = csp_directives["script-src"] .. " https://hcaptcha.com https://*.hcaptcha.com"
csp_directives["frame-src"] = "https://hcaptcha.com https://*.hcaptcha.com"
csp_directives["style-src"] = csp_directives["style-src"] .. " https://hcaptcha.com https://*.hcaptcha.com"
csp_directives["connect-src"] = "https://hcaptcha.com https://*.hcaptcha.com"
elseif self.session_data.type == "turnstile" then
ngx.header[header] = "default-src 'none'; form-action 'self'; script-src 'strict-dynamic' 'nonce-"
.. self.session_data.nonce_script
.. "' https://challenges.cloudflare.com 'unsafe-inline' http: https:; img-src 'self' data:;"
.. " frame-src https://challenges.cloudflare.com; style-src 'self' 'nonce-"
.. self.session_data.nonce_style
.. "'; font-src 'self' data:; base-uri 'self';"
else
ngx.header[header] = "default-src 'none'; form-action 'self'; script-src 'strict-dynamic' 'nonce-"
.. self.session_data.nonce_script
.. "' 'unsafe-inline' http: https:; img-src 'self' data:; style-src 'self' 'nonce-"
.. self.session_data.nonce_style
.. "'; font-src 'self' data:; base-uri 'self';"
csp_directives["script-src"] = csp_directives["script-src"] .. " https://challenges.cloudflare.com"
csp_directives["frame-src"] = "https://challenges.cloudflare.com"
end
local csp_content = ""
for directive, value in pairs(csp_directives) do
csp_content = csp_content .. directive .. " " .. value .. "; "
end
ngx.header["Content-Security-Policy"] = csp_content
return self:ret(true, "successfully overridden CSP header")
end

Expand Down Expand Up @@ -192,6 +186,9 @@ function antibot:content()
return self:ret(true, "no session", nil, "/")
end

self.ctx.bw.antibot_nonce_script = rand(32)
self.ctx.bw.antibot_nonce_style = rand(32)

-- Display content
local ok, err = self:display_challenge()
if not ok then
Expand Down Expand Up @@ -242,8 +239,6 @@ function antibot:prepare_challenge()
self.session_data.type = self.variables["USE_ANTIBOT"]
self.session_data.resolved = false
self.session_data.original_uri = self.ctx.bw.request_uri
self.session_data.nonce_script = rand(16)
self.session_data.nonce_style = rand(16)
if self.ctx.bw.uri == self.variables["ANTIBOT_URI"] then
self.session_data.original_uri = "/"
end
Expand All @@ -268,8 +263,8 @@ function antibot:display_challenge()
-- Common variables for templates
local template_vars = {
antibot_uri = self.variables["ANTIBOT_URI"],
nonce_script = self.session_data.nonce_script,
nonce_style = self.session_data.nonce_style,
nonce_script = self.ctx.bw.antibot_nonce_script,
nonce_style = self.ctx.bw.antibot_nonce_style,
}

-- Javascript case
Expand Down Expand Up @@ -387,7 +382,10 @@ function antibot:check_challenge()
if not ok then
return nil, "error while decoding JSON from reCAPTCHA API : " .. rdata, nil
end
if not rdata.success or rdata.score < tonumber(self.variables["ANTIBOT_RECAPTCHA_SCORE"]) then
if not rdata.success then
return false, "client failed challenge", nil
end
if rdata.score and rdata.score < tonumber(self.variables["ANTIBOT_RECAPTCHA_SCORE"]) then
return false, "client failed challenge with score " .. tostring(rdata.score), nil
end
self.session_data.resolved = true
Expand Down
113 changes: 71 additions & 42 deletions src/common/core/antibot/files/recaptcha.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,65 @@
margin-top: 1rem
}
</style>
{-raw-}
<script
src="https://www.google.com/recaptcha/api.js?onload=onv3Callback&render={*recaptcha_sitekey*}&trustedtypes=true"
nonce="{*nonce_script*}"
></script>
<script
defer
src="https://www.google.com/recaptcha/api.js?onload=onv2Callback&trustedtypes=true"
nonce="{*nonce_script*}"
></script>
<script type="text/javascript" nonce="{*nonce_script*}">
var onSubmit = function (token) {
document.getElementById("token").value = token;
document.getElementById("form").submit();
};

var usedVersion = "v2";

var onv2Callback = function () {
if (usedVersion === "invisible") {
return;
}

// Remove the v3 button
document.getElementById("recaptcha-verify").remove();

// Insert the v2 div
var recaptchaDiv = document.createElement("div");
recaptchaDiv.id = "recaptcha-v2-verify";
recaptchaDiv.className = "g-recaptcha";
recaptchaDiv.setAttribute("data-sitekey", "{*recaptcha_sitekey*}");
recaptchaDiv.setAttribute("data-callback", "onSubmit");
recaptchaDiv.setAttribute("data-action", "submit");
document
.getElementById("recaptcha-container")
.appendChild(recaptchaDiv);

grecaptcha.ready(function () {
grecaptcha.render("recaptcha-v2-verify", {
sitekey: "{*recaptcha_sitekey*}",
callback: onSubmit,
action: "submit",
});
document.querySelector(".grecaptcha-badge").remove();
});
};

var onv3Callback = function () {
usedVersion = "invisible";
grecaptcha.ready(function () {
document
.getElementById("recaptcha-verify")
.addEventListener("click", function () {
grecaptcha.execute();
});
});
};
</script>
{-raw-}
</head>
<body
class="bg-gradient-to-r from-[#075577] to-[#116D70] w-screen h-screen overflow-hidden"
Expand Down Expand Up @@ -255,14 +314,19 @@
<form class="hidden" method="POST" action="{*antibot_uri*}" id="form">
<input type="hidden" name="token" id="token" />
</form>
{-raw-}
<div class="mt-8 flex flex-col justify-center items-center">
<button
id="recaptcha-verify"
class="text-sm xs:text-base mb-2.5 hover:brightness-90 mt-2 rounded-lg bg-secondary px-6 py-2 text-white font-bold"
>
I'm not a robot
</button>
<div id="recaptcha-container">
<button
id="recaptcha-verify"
class="g-recaptcha text-sm xs:text-base mb-2.5 hover:brightness-90 mt-2 rounded-lg bg-secondary px-6 py-2 text-white font-bold"
data-sitekey="{*recaptcha_sitekey*}"
data-callback="onSubmit"
data-action="submit"
>
I'm not a robot
</button>
</div>
{-raw-}
<p
id="recaptcha-terms"
class="text-gray-100 text-center text-xs sm:text-sm"
Expand All @@ -284,41 +348,6 @@
</div>
</div>

{-raw-}
<script nonce="{*nonce_script*}">
// recaptcha
const check_robot = function () {
grecaptcha.ready(function () {
grecaptcha
.execute("{*recaptcha_sitekey*}", { action: "recaptcha" })
.then(function (token) {
document.getElementById("token").value = token;
document.getElementById("form").submit();
});
});
};

var cooldown = false;

document
.getElementById("recaptcha-verify")
.addEventListener("click", (e) => {
e.preventDefault();
if (cooldown) return;
cooldown = true;
check_robot();
setTimeout(() => {
cooldown = false;
}, 1500);
});
</script>
<script
async
src="https://www.google.com/recaptcha/api.js?render={*recaptcha_sitekey*}"
nonce="{*nonce_script*}"
></script>
{-raw-}

<!-- text -->
<footer class="fixed bottom-1.5 lg:bottom-2">
<div class="flex justify-center pb-2">
Expand Down
1 change: 0 additions & 1 deletion src/common/core/antibot/files/turnstile.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
</style>
{-raw-}
<script
async
defer
src="https://challenges.cloudflare.com/turnstile/v0/api.js"
nonce="{*nonce_script*}"
Expand Down
4 changes: 2 additions & 2 deletions src/common/core/antibot/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@
"ANTIBOT_RECAPTCHA_SCORE": {
"context": "multisite",
"default": "0.7",
"help": "Minimum score required for reCAPTCHA challenge.",
"help": "Minimum score required for reCAPTCHA challenge (Only compatible with reCAPTCHA v3).",
"id": "antibot-recaptcha-score",
"label": "reCAPTCHA score",
"label": "reCAPTCHA v3 score",
"regex": "^(0\\.[1-9]|1\\.0)$",
"type": "text"
},
Expand Down
Loading

0 comments on commit 18517b9

Please sign in to comment.