Skip to content

Commit

Permalink
core: add timer on pw checks against timing attacks for finding valid…
Browse files Browse the repository at this point in the history
… usernames (0.x) (#3097)

* core: add timer on pw checks against timing attacks for finding valid usernames

* mod_ratelimit: register logon try before checking password.

This prevents multiple parallel password tries in the ~300msec period that the password is checked.

* Fix ratelimit precheck
  • Loading branch information
mworrell committed Aug 29, 2022
1 parent 6d966c4 commit 7c02a36
Show file tree
Hide file tree
Showing 2 changed files with 33 additions and 8 deletions.
11 changes: 4 additions & 7 deletions modules/mod_ratelimit/mod_ratelimit.erl
Original file line number Diff line number Diff line change
Expand Up @@ -71,17 +71,14 @@ observe_auth_precheck( #auth_precheck{ username = Username }, Context ) ->
Context),
{error, ratelimit};
false ->
m_ratelimit:insert_event(auth, Username, DeviceId, Context),
undefined
end.

%% @doc Handle the result of the password authentication, register all failures
observe_auth_checked( #auth_checked{ username = Username, is_accepted = false }, Context ) ->
DeviceId = case validate_device_cookie(Context) of
{ok, #rldid{ username = Username, device_id = DId }} -> DId;
{ok, _} -> undefined;
{error, _} -> undefined
end,
m_ratelimit:insert_event(auth, Username, DeviceId, Context);
observe_auth_checked( #auth_checked{ username = _Username, is_accepted = false }, _Context ) ->
% We already registered a try at the precheck.
ok;
observe_auth_checked( #auth_checked{ username = Username, is_accepted = true }, Context ) ->
% Store the authenticated username for later retrieval in observe_auth_logon.
z_context:set_session(ratelimit_event_username, Username, Context).
Expand Down
30 changes: 29 additions & 1 deletion src/models/m_identity.erl
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@
-include_lib("zotonic.hrl").


%% Default duration and random variance interval for password checks.
%% This prevents a timing difference between checks for existing and
%% non existing accounts.
-define(DEFAULT_PW_CHECK_DURATION, 280).
-define(DEFAULT_PW_CHECK_VARIANCE, 40).


%% @doc Fetch the value for the key from a model source
%% @spec m_find_value(Key, Source, Context) -> term()
m_find_value(lookup, #m{value=undefined} = M, _Context) ->
Expand Down Expand Up @@ -473,11 +480,32 @@ nodash(S) ->
check_username_pw(Username, Password, Context) ->
check_username_pw(Username, Password, [], Context).


%% @doc Return the rsc_id with the given username/password.
%% If succesful then updates the 'visited' timestamp of the entry.
%% If succesful then updates the 'visited' timestamp of the entry.
%% Use a timer to level the time difference between existing and non existing accounts.
-spec check_username_pw(binary() | string(), binary() | string(), list(), z:context()) ->
{ok, m_rsc:resource_id()} | {error, term()}.
check_username_pw(Username, Password, QueryArgs, Context) ->
Timeout = ?DEFAULT_PW_CHECK_DURATION + z_ids:number(?DEFAULT_PW_CHECK_VARIANCE),
Ref = erlang:make_ref(),
erlang:send_after(Timeout, self(), {pw_done, Ref}),
Result = check_username_pw_do(Username, Password, QueryArgs, Context),
wait_message(Ref),
Result.

wait_message(Ref) ->
receive
{pw_done, Ref} ->
ok;
{pw_done, _} ->
wait_message(Ref)
after ?DEFAULT_PW_CHECK_DURATION + ?DEFAULT_PW_CHECK_DURATION ->
lager:error("Timeout waiting for pw_done message."),
ok
end.

check_username_pw_do(Username, Password, QueryArgs, Context) ->
NormalizedUsername = z_convert:to_binary( z_string:trim( z_string:to_lower(Username) ) ),
case z_notifier:first(#auth_precheck{ username = NormalizedUsername }, Context) of
Ok when Ok =:= ok; Ok =:= undefined ->
Expand Down

0 comments on commit 7c02a36

Please sign in to comment.