mirror of
https://github.com/bunkerity/bunkerweb
synced 2026-05-24 09:28:37 +00:00
Add support for reCAPTCHA v2 and invisible
This commit is contained in:
parent
f84ec9813b
commit
2c3fe6bfe0
6 changed files with 110 additions and 84 deletions
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
113
src/common/core/antibot/files/recaptcha.html
vendored
113
src/common/core/antibot/files/recaptcha.html
vendored
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
1
src/common/core/antibot/files/turnstile.html
vendored
1
src/common/core/antibot/files/turnstile.html
vendored
|
|
@ -22,7 +22,6 @@
|
|||
</style>
|
||||
{-raw-}
|
||||
<script
|
||||
async
|
||||
defer
|
||||
src="https://challenges.cloudflare.com/turnstile/v0/api.js"
|
||||
nonce="{*nonce_script*}"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue