Skip to content

Commit

Permalink
Add options for beginning to enforce recaptcha validations.
Browse files Browse the repository at this point in the history
- Allow configurable origin hosts for determining when recaptcha should
  be required.
- Remove deprecated `ssl_handshake` usage for lua-resty-http calls (it
  is now integrated into the connect call).
  • Loading branch information
GUI committed Dec 2, 2023
1 parent d7f9fe6 commit d369c55
Show file tree
Hide file tree
Showing 8 changed files with 818 additions and 23 deletions.
9 changes: 9 additions & 0 deletions config/schema.cue
Original file line number Diff line number Diff line change
Expand Up @@ -369,8 +369,17 @@ import "path"
default_host?: string
send_notify_email?: bool
admin_notify_email?: string
#scheme: "http" | "https"
recaptcha_scheme: #scheme | *"https"
recaptcha_host: string | *"www.google.com"
recaptcha_port: uint16 | *443
recaptcha_v2_secret_key?: string
recaptcha_v2_required: bool | *false
recaptcha_v2_required_origin_regex?: string
recaptcha_v3_secret_key?: string
recaptcha_v3_required: bool | *false
recaptcha_v3_required_score: float | *0.9
recaptcha_v3_required_origin_regex?: string
}

static_site: {
Expand Down
10 changes: 2 additions & 8 deletions src/api-umbrella/utils/elasticsearch.lua
Original file line number Diff line number Diff line change
Expand Up @@ -59,20 +59,14 @@ function _M.query(path, options)
scheme = server["scheme"],
host = server["host"],
port = server["port"],
ssl_server_name = server["host"],
ssl_verify = true,
})
if not connect_ok then
httpc:close()
return nil, "elasticsearch connect error: " .. (connect_err or "")
end

if server["scheme"] == "https" then
local ssl_ok, ssl_err = httpc:ssl_handshake(nil, server["host"], true)
if not ssl_ok then
httpc:close()
return nil, "elasticsearch ssl handshake error: " .. (ssl_err or "")
end
end

local res, err = httpc:request(options)
if err then
httpc:close()
Expand Down
96 changes: 81 additions & 15 deletions src/api-umbrella/web-app/actions/v1/users.lua
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ local is_array = require "api-umbrella.utils.is_array"
local is_email = require "api-umbrella.utils.is_email"
local is_empty = require "api-umbrella.utils.is_empty"
local is_hash = require "api-umbrella.utils.is_hash"
local json_decode = require("cjson").decode
local json_decode = require("cjson.safe").decode
local json_encode = require "api-umbrella.utils.json_encode"
local json_response = require "api-umbrella.web-app.utils.json_response"
local known_domains = require "api-umbrella.web-app.utils.known_domains"
Expand All @@ -32,6 +32,7 @@ local wrapped_json_params = require "api-umbrella.web-app.utils.wrapped_json_par

local db_null = db.NULL
local gsub = ngx.re.gsub
local re_find = ngx.re.find

local _M = {}

Expand Down Expand Up @@ -135,20 +136,17 @@ local function verify_recaptcha(secret, response)
end

local connect_ok, connect_err = httpc:connect({
scheme = "https",
host = "www.google.com",
scheme = config["web"]["recaptcha_scheme"],
host = config["web"]["recaptcha_host"],
port = config["web"]["recaptcha_port"],
ssl_server_name = config["web"]["recaptcha_host"],
ssl_verify = true,
})
if not connect_ok then
httpc:close()
return nil, "recaptcha connect error: " .. (connect_err or "")
end

local ssl_ok, ssl_err = httpc:ssl_handshake(nil, "www.google.com", true)
if not ssl_ok then
httpc:close()
return nil, "recaptcha ssl handshake error: " .. (ssl_err or "")
end

local res, err = httpc:request({
method = "POST",
path = "/recaptcha/api/siteverify",
Expand Down Expand Up @@ -182,10 +180,61 @@ local function verify_recaptcha(secret, response)
return nil, "Unsuccessful response: " .. (body or "")
end

local data = json_decode(body)
local data, json_err = json_decode(body)
if json_err then
return nil, "recaptcha json error: " .. (json_err or "")
end

return data
end

local function recaptcha_required_for_origin(required_origin_regex, request_origin)
if not required_origin_regex then
return true
end

local find_from, _, find_err = re_find(request_origin or "", required_origin_regex, "ijo")
if find_err then
ngx.log(ngx.ERR, "regex error: ", find_err)
return false
end

if find_from then
return true
else
return false
end
end

local function recaptcha_passes(self, user_params)
-- Admins don't need captcha.
if self.current_admin then
return true
end

if config["web"]["recaptcha_v2_required"] and recaptcha_required_for_origin(config["web"]["recaptcha_v2_required_origin_regex"], user_params["registration_origin"]) then
if not user_params["registration_recaptcha_v2_success"] then
return false, "reCAPTCHA v2 not successful"
elseif not known_domains.is_allowed_domain(user_params["registration_recaptcha_v2_hostname"]) then
return false, "reCAPTCHA v2 disallowed domain: " .. (user_params["registration_recaptcha_v2_hostname"] or "")
end
end

if config["web"]["recaptcha_v3_required"] and recaptcha_required_for_origin(config["web"]["recaptcha_v3_required_origin_regex"], user_params["registration_origin"]) then
if not user_params["registration_recaptcha_v3_success"] then
return false, "reCAPTCHA v3 not successful"
elseif not known_domains.is_allowed_domain(user_params["registration_recaptcha_v3_hostname"]) then
return false, "reCAPTCHA v3 disallowed domain: " .. (user_params["registration_recaptcha_v3_hostname"] or "")
elseif not user_params["registration_recaptcha_v3_score"] then
return false, "reCAPTCHA v3 missing score"
elseif user_params["registration_recaptcha_v3_score"] < config["web"]["recaptcha_v3_required_score"] then
return false, "reCAPTCHA v3 below required score: " .. (user_params["registration_recaptcha_v3_score"] or "")
end
end

return true
end

function _M.index(self)
return datatables.index(self, ApiUser, {
where = {
Expand Down Expand Up @@ -294,14 +343,31 @@ function _M.create(self)
end
end

local recaptcha_ok, recaptcha_err = recaptcha_passes(self, user_params)
if not recaptcha_ok then
ngx.log(ngx.WARN, "reCAPTCHA failed: ", (recaptcha_err or "") .. "; " .. json_encode(user_params) .. "; " .. json_encode(request_headers))
return coroutine.yield("error", {
_render = {
errors = {
{
code = "UNEXPECTED_ERROR",
message = t("CAPTCHA verification failed. Please try again or contact us for assistance."),
},
},
},
})
end

if not self.current_admin and request_headers["referer"] and (not request_headers["user-agent"] or not request_headers["origin"]) then
ngx.log(ngx.WARN, "Missing `User-Agent` or `Origin`: " .. json_encode(request_headers) .. "; " .. json_encode(user_params))
return coroutine.yield("error", {
{
code = "UNEXPECTED_ERROR",
field = "email",
field_label = "email",
message = t("An unexpected error occurred during signup. Please try again or contact us for assistance."),
_render = {
errors = {
{
code = "UNEXPECTED_ERROR",
message = t("An unexpected error occurred during signup. Please try again or contact us for assistance."),
},
},
},
})
end
Expand Down
19 changes: 19 additions & 0 deletions templates/etc/test-env/nginx/apis.conf.etlua
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,25 @@ server {
}
}

location = /recaptcha/api/siteverify/set-mock {
content_by_lua_block {
local cjson = require "cjson"
ngx.req.read_body()
local body = ngx.req.get_body_data()
ngx.shared.test_data:set("recaptcha_mock", body)
}
}

location = /recaptcha/api/siteverify {
content_by_lua_block {
local cjson = require "cjson"
local mock = cjson.decode(ngx.shared.test_data:get("recaptcha_mock"))
ngx.status = mock["status"]
ngx.print(mock["body"])
ngx.exit(ngx.HTTP_OK)
}
}

location = / {
echo -n "Test Home Page";
}
Expand Down
9 changes: 9 additions & 0 deletions test/apis/v1/users/test_create.rb
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,15 @@ def test_rejects_empty_origin_for_non_admins
:body => MultiJson.dump(:user => attributes),
}))
assert_response_code(422, response)
data = MultiJson.load(response.body)
assert_equal({
"errors" => [{
"code" => "INVALID_INPUT",
"field" => "first_name",
"message" => "is too long (maximum is 80 characters)",
"full_message" => "First name: is too long (maximum is 80 characters)",
}],
}, data)
end

def test_accepts_empty_origin_for_admins
Expand Down
Loading

0 comments on commit d369c55

Please sign in to comment.