Skip to content

Commit 8a265b8

Browse files
committed
fix: prevent token from being used more than once for account create
Since invite is held in state of connection, one could potentitally authorize and unlimited amount of sessions and then create accounts. Now account creation checks within a transaction or atomic update that token is still valid on update. Also, interface has been unified
1 parent 76c5fe9 commit 8a265b8

File tree

6 files changed

+157
-73
lines changed

6 files changed

+157
-73
lines changed

src/mod_invites.erl

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
%% helpers
4646
-export([create_account_allowed/2, get_invite/2, get_invites/2, get_max_invites/2, is_create_allowed/2,
4747
is_expired/1, is_reserved/3, is_token_valid/2, roster_add/2, send_presence/3,
48-
set_invitee/3, update_invite/2, token_uri/1, xdata_field/3]).
48+
set_invitee/3, set_invitee/5, token_uri/1, xdata_field/3]).
4949

5050
%% ejabberd_http
5151
-export([process/2]).
@@ -76,8 +76,12 @@
7676
-callback is_token_valid(Host :: binary(), binary(), {binary(), binary()}) -> boolean().
7777
-callback list_invites(Host :: binary()) -> [tuple()].
7878
-callback remove_user(User :: binary(), Server :: binary()) -> any().
79-
-callback set_invitee(Host :: binary(), Token :: binary(), Invitee :: binary()) -> ok.
80-
-callback update_invite(Host :: binary(), Invite :: invite_token()) -> ok.
79+
-callback set_invitee(Fun :: fun(() -> {ok|error, Res}),
80+
Host :: binary(),
81+
Token :: binary(),
82+
Invitee :: binary(),
83+
AccountName :: binary()) -> Res
84+
when Res :: any().
8185

8286
%% @format-begin
8387

@@ -101,9 +105,7 @@ mod_doc() ->
101105
" create such invites. Furthermore it applies to 'roster invites' and allows "
102106
"to do in-band registration (ibr) if the sending user is allowed by this rule. "
103107
"Users from the 'admin' ACL are always allowed to create account invites."),
104-
example =>
105-
["mod_invites:",
106-
" access_create_account: local"]}},
108+
example => ["mod_invites:", " access_create_account: local"]}},
107109
{db_type,
108110
#{value => "mnesia | sql",
109111
desc =>
@@ -623,14 +625,23 @@ is_token_valid(Host, Token) ->
623625
is_token_valid(Host, Token, Inviter) ->
624626
db_call(Host, is_token_valid, [Host, Token, Inviter]).
625627

626-
set_invitee(Host, Token, InviteeJid) ->
627-
%% This invalidates the invite token
628-
db_call(Host,
629-
set_invitee,
630-
[Host,
631-
Token,
632-
jid:encode(
633-
jid:remove_resource(InviteeJid))]).
628+
-spec set_invitee(binary(), binary(), jid() | binary()) -> ok.
629+
set_invitee(Host, Token, #jid{} = InviteeJid) ->
630+
set_invitee(Host,
631+
Token,
632+
jid:encode(
633+
jid:remove_resource(InviteeJid)),
634+
<<>>);
635+
set_invitee(Host, Token, Invitee) ->
636+
set_invitee(Host, Token, Invitee, <<>>).
637+
638+
set_invitee(Host, Token, Invitee, AccountName) ->
639+
set_invitee(fun() -> ok end, Host, Token, Invitee, AccountName).
640+
641+
-spec set_invitee(binary(), binary(), binary(), binary()) -> ok.
642+
set_invitee(F, Host, Token, Invitee, AccountName) ->
643+
%% This invalidates the invite token if Invitee isn't empty
644+
db_call(Host, set_invitee, [F, Host, Token, Invitee, AccountName]).
634645

635646
create_roster_invite(Host, Inviter) ->
636647
create_invite(roster_only, Host, Inviter, <<>>).
@@ -748,9 +759,6 @@ invite_token(Type, Host, Inviter, AccountName0) ->
748759
account_name = AccountName},
749760
mod_invites_opt:token_expire_seconds(Host)).
750761

751-
update_invite(Host, Invite) ->
752-
db_call(Host, update_invite, [Host, Invite]).
753-
754762
token_uri(#invite_token{type = Type,
755763
token = Token,
756764
account_name = AccountName,

src/mod_invites_http.erl

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -179,12 +179,8 @@ process_register_post(Invite,
179179
ensure_same(Token, proplists:get_value(<<"token">>, Q))}
180180
of
181181
{AppCtx, ok} ->
182-
case mod_register:try_register(Username, Host, Password, Source, mod_invites, Lang) of
183-
ok ->
184-
InviteeJid = jid:make(Username, Host),
185-
mod_invites:set_invitee(Host, Token, InviteeJid),
186-
UpdatedInvite = mod_invites:get_invite(Host, Token),
187-
mod_invites_register:maybe_create_mutual_subscription(UpdatedInvite),
182+
case mod_invites_register:try_register(Invite, Username, Host, Password, Source, Lang) of
183+
{ok, _UpdatedInvite} ->
188184
Ctx = [{username, Username}, {password, Password} | AppCtx],
189185
render_ok(Host, Lang, <<"register_success.html">>, Ctx);
190186
{error, #stanza_error{text = Text, type = Type} = Error} ->

src/mod_invites_mnesia.erl

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929

3030
-export([cleanup_expired/1, create_invite/1, expire_tokens/2, get_invite/2, get_invites/2, init/2,
3131
is_reserved/3, is_token_valid/3, list_invites/1, remove_user/2,
32-
set_invitee/3, update_invite/2]).
32+
set_invitee/5]).
3333

3434
-include("mod_invites.hrl").
3535

@@ -113,14 +113,23 @@ remove_user(User, Server) ->
113113
<- mnesia:dirty_index_read(invite_token, Inviter, #invite_token.inviter)],
114114
ok.
115115

116-
set_invitee(_Host, Token, Invitee) ->
116+
-spec set_invitee(fun(), binary(), binary(), binary(), binary()) -> ok.
117+
set_invitee(F, _Host, Token, Invitee, AccountName) ->
117118
Transaction =
118119
fun() ->
119-
[Invite] = mnesia:read(invite_token, Token),
120-
mnesia:write(Invite#invite_token{invitee = Invitee})
120+
[#invite_token{invitee = <<>>, type = Type} = Invite] = mnesia:read(invite_token, Token),
121+
case Type of
122+
roster_only ->
123+
<<>> = Invite#invite_token.account_name;
124+
_ ->
125+
ok
126+
end,
127+
case F() of
128+
ok ->
129+
ok = mnesia:write(Invite#invite_token{invitee = Invitee, account_name = AccountName});
130+
{error, _Res} = Error ->
131+
Error
132+
end
121133
end,
122-
{atomic, ok} = mnesia:transaction(Transaction),
123-
ok.
124-
125-
update_invite(_Host, Invite) ->
126-
mnesia:dirty_write(Invite).
134+
{atomic, Res} = mnesia:transaction(Transaction),
135+
Res.

src/mod_invites_register.erl

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
-author('[email protected]').
2828

2929
-export([c2s_unauthenticated_packet/2, stream_feature_register/2]).
30-
-export([maybe_create_mutual_subscription/1]).
30+
-export([try_register/6]).
3131

3232
-import(mod_invites, [roster_add/2, send_presence/3, xdata_field/3]).
3333
-include("logger.hrl").
@@ -79,28 +79,21 @@ c2s_unauthenticated_packet(#{invite := Invite} = State,
7979
{stop, ejabberd_c2s:send(State, ResIQ1)}
8080
end);
8181
c2s_unauthenticated_packet(#{invite := Invite, server := Server} = State,
82-
#iq{type = set, sub_els = [_]} = IQ) ->
82+
#iq{type = set, sub_els = [_], lang = Lang} = IQ) ->
8383
%% Process registration request after processing token
8484
?TRY_SUBTAG(IQ,
8585
#register{},
8686
fun(Register) ->
8787
case check_captcha(mod_register_opt:captcha_protected(Server), Register, IQ) of
8888
{ok, {Username, Password}} ->
89-
Token = Invite#invite_token.token,
9089
#{ip := IP} = State,
9190
{Address, _} = IP,
92-
case try_register(Token, Username, Server, Password, IQ, Address) of
93-
#iq{type = result} = ResIQ ->
94-
NewInvite =
95-
maybe_set_account_name(maybe_set_invitee(Invite,
96-
jid:make(Username,
97-
Server)),
98-
Username),
99-
ok = mod_invites:update_invite(Server, NewInvite),
100-
ResState = State#{invite => NewInvite},
101-
maybe_create_mutual_subscription(NewInvite),
102-
{stop, ejabberd_c2s:send(ResState, ResIQ)};
103-
#iq{type = error} = ResIQ ->
91+
case try_register(Invite, Username, Server, Password, Address, Lang) of
92+
{ok, UpdatedInvite} ->
93+
ResState = State#{invite => UpdatedInvite},
94+
{stop, ejabberd_c2s:send(ResState, xmpp:make_iq_result(IQ))};
95+
{error, Err} ->
96+
ResIQ = make_stripped_error(IQ, #register{}, Err),
10497
{stop, ejabberd_c2s:send(State, ResIQ)}
10598
end;
10699
{error, ResIQ} ->
@@ -210,18 +203,23 @@ preauth_invalid(IQ, Lang) ->
210203
Text = ?T("The token provided is either invalid or expired."),
211204
make_stripped_error(IQ, #preauth{}, xmpp:err_item_not_found(Text, Lang)).
212205

213-
try_register(Token, User, Server, Password, #iq{lang = Lang} = IQ, Source) ->
206+
try_register(Invite, User, Server, Password, Source, Lang) ->
207+
#invite_token{token = Token} = Invite,
214208
case {jid:nodeprep(User), not mod_invites:is_reserved(Server, Token, User)} of
215209
{error, _} ->
216-
Err = xmpp:err_jid_malformed(
217-
mod_register:format_error(invalid_jid), Lang),
218-
make_stripped_error(IQ, #register{}, Err);
210+
{error, xmpp:err_jid_malformed(mod_register:format_error(invalid_jid), Lang)};
219211
{_, true} ->
220-
case mod_register:try_register(User, Server, Password, Source, mod_invites, Lang) of
212+
RegF =
213+
fun() -> mod_register:try_register(User, Server, Password, Source, mod_invites, Lang)
214+
end,
215+
NewInvite = #invite_token{invitee = Invitee, account_name = AccountName} =
216+
maybe_set_account_name(maybe_set_invitee(Invite, jid:make(User, Server)), User),
217+
case mod_invites:set_invitee(RegF, Server, Token, Invitee, AccountName) of
221218
ok ->
222-
xmpp:make_iq_result(IQ);
223-
{error, Error} ->
224-
make_stripped_error(IQ, #register{}, Error)
219+
maybe_create_mutual_subscription(NewInvite),
220+
{ok, NewInvite};
221+
{error, _Reason} = Error ->
222+
Error
225223
end
226224
end.
227225

src/mod_invites_sql.erl

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929

3030
-export([cleanup_expired/1, create_invite/1, expire_tokens/2, get_invite/2, get_invites/2, init/2,
3131
is_reserved/3, is_token_valid/3, list_invites/1, remove_user/2,
32-
set_invitee/3, update_invite/2]).
32+
set_invitee/5]).
3333

3434
-export([sql_schemas/0]).
3535

@@ -186,22 +186,20 @@ remove_user(User, Server) ->
186186
ejabberd_sql:sql_query(Server,
187187
?SQL("DELETE FROM invite_token WHERE username=%(User)s AND %(Server)H")).
188188

189-
set_invitee(Host, Token, Invitee) ->
190-
{updated, 1} =
191-
ejabberd_sql:sql_query(Host,
192-
?SQL("UPDATE invite_token SET invitee=%(Invitee)s WHERE token=%(Token)s")),
193-
ok.
194-
195-
update_invite(Host, Invite) ->
196-
#invite_token{token = Token,
197-
invitee = Invitee,
198-
account_name = AccountName} =
199-
Invite,
200-
Query =
201-
?SQL("UPDATE invite_token SET invitee = %(Invitee)s, account_name = %(AccountName)s "
202-
"WHERE %(Host)H AND token = %(Token)s"),
203-
{updated, 1} = ejabberd_sql:sql_query(Host, Query),
204-
ok.
189+
set_invitee(Fun, Host, Token, Invitee, AccountName) ->
190+
Trans =
191+
fun() ->
192+
{updated, 1} =
193+
ejabberd_sql:sql_query_t(?SQL("UPDATE invite_token SET invitee = %(Invitee)s, account_name = %(AccountName)s WHERE "
194+
"%(Host)H AND token = %(Token)s AND invitee = '' AND (type != 'R' OR account_name = '')")),
195+
ok = Fun()
196+
end,
197+
case ejabberd_sql:sql_transaction(Host, Trans) of
198+
{atomic, Res} ->
199+
Res;
200+
{aborted, {badmatch, {error, _Res} = Error}} ->
201+
Error
202+
end.
205203

206204
%%--------------------------------------------------------------------
207205
%%| helpers

test/invites_tests.erl

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ single_cases() ->
6666
single_test(ibr),
6767
single_test(ibr_reserved),
6868
single_test(ibr_subscription),
69+
single_test(ibr_conflict),
6970
single_test(http)]}.
7071

7172
%%%===================================================================
@@ -296,7 +297,9 @@ max_invites(Config0) ->
296297
?match(#iq{type = error}, send_pars(Config, RosterInviteToken)),
297298

298299
%% we can create more than 3 as an "admin" user
299-
?match(0, length([error || _ <- lists:seq(1, 4), element(1, mod_invites:gen_invite(Server)) == error])),
300+
?match(0,
301+
length([error
302+
|| _ <- lists:seq(1, 4), element(1, mod_invites:gen_invite(Server)) == error])),
300303

301304
update_module_opts(Server, mod_invites, OldOpts),
302305
#invite_token{} = create_account_invite(Server, Inviter),
@@ -365,6 +368,7 @@ stream_feature(Config0) ->
365368

366369
ibr(Config0) ->
367370
Server = ?config(server, Config0),
371+
User = ?config(user, Config0),
368372
AccountName = <<"new_user">>,
369373

370374
OldRegisterOpts = gen_mod:get_module_opts(Server, mod_register),
@@ -380,6 +384,10 @@ ibr(Config0) ->
380384
#invite_token{token = RosterToken} =
381385
mod_invites:create_roster_invite(Server, {<<"inviter">>, Server}),
382386
?match(#iq{type = result}, send_pars(Config1, RosterToken)),
387+
?match(#iq{type = result}, send_iq_register(Config1, <<"roster_invite_user">>)),
388+
%% roster tokens should still be valid because they will be used for roster pars later by the
389+
%% client
390+
?match(true, mod_invites:is_token_valid(Server, RosterToken)),
383391

384392
Config11 = reconnect(Config1),
385393

@@ -401,7 +409,10 @@ ibr(Config0) ->
401409
?match(#iq{type = result}, send_pars(Config2, Token2)),
402410
?match(#iq{type = result, sub_els = [#register{username = <<"some_unfavorable_name">>}]},
403411
send_get_iq_register(Config2)),
412+
?match(#iq{type = error}, send_iq_register(Config2, User)),
413+
?match(#iq{type = error}, send_iq_register(Config2, <<"@invalid_user">>)),
404414
?match(#iq{type = result}, send_iq_register(Config2, <<"some_much_better_name">>)),
415+
?match(#iq{type = error}, send_iq_register(Config2, <<"one_more_try">>)),
405416

406417
Config3 = reconnect(Config2),
407418
#invite_token{token = Token3} = create_account_invite(Server, {<<>>, Server}),
@@ -538,6 +549,70 @@ receive_subscription_stanzas(Count,
538549
end,
539550
receive_subscription_stanzas(Count - 1, Res, ServerJID, UserFullJID, NewAccountFullJID).
540551

552+
ibr_conflict(Config0) ->
553+
Server = ?config(server, Config0),
554+
555+
OldRegisterOpts = gen_mod:get_module_opts(Server, mod_register),
556+
NewRegisterOpts = gen_mod:set_opt(allow_modules, [mod_invites], OldRegisterOpts),
557+
update_module_opts(Server, mod_register, NewRegisterOpts),
558+
559+
Config1 = reconnect(Config0),
560+
561+
#invite_token{token = Token} =
562+
mod_invites:create_account_invite(Server, {<<>>, Server}, <<>>, false),
563+
?match(#iq{type = result}, send_pars(Config1, Token)),
564+
Parent = self(),
565+
Pid = spawn(fun() ->
566+
Config11 = connect(lists:keydelete(receiver, 1, Config1)),
567+
#iq{type = result} = send_pars(Config11, Token),
568+
#iq{type = Type} = send_iq_register(Config11, <<"ibr_conflict_1">>),
569+
Parent ! {self(), Type},
570+
disconnect(Config11)
571+
end),
572+
receive
573+
{Pid, Result} ->
574+
?match(result, Result)
575+
after 1000 ->
576+
ct:fail(timeout)
577+
end,
578+
579+
?match(#iq{type = result}, send_get_iq_register(Config1)),
580+
?match(#iq{type = error}, send_iq_register(Config1, <<"ibr_conflict_2">>)),
581+
?match(false, ejabberd_auth:user_exists(<<"ibr_conflict_2">>, Server)),
582+
?match(false, mod_invites:is_token_valid(Server, Token)),
583+
584+
Config2 = reconnect(Config1),
585+
586+
#invite_token{token = RosterToken} =
587+
mod_invites:create_roster_invite(Server, {<<"inviter">>, Server}),
588+
?match(#iq{type = result}, send_pars(Config2, RosterToken)),
589+
590+
Pid2 =
591+
spawn(fun() ->
592+
Config21 = connect(lists:keydelete(receiver, 1, Config2)),
593+
#iq{type = result} = send_pars(Config21, RosterToken),
594+
#iq{type = Type} = send_iq_register(Config21, <<"ibr_conflict_3">>),
595+
Parent ! {self(), Type},
596+
disconnect(Config21)
597+
end),
598+
receive
599+
{Pid2, Result2} ->
600+
?match(result, Result2)
601+
after 1000 ->
602+
ct:fail(timeout)
603+
end,
604+
605+
?match(#iq{type = result}, send_get_iq_register(Config2)),
606+
?match(#iq{type = error}, send_iq_register(Config2, <<"ibr_conflict_4">>)),
607+
?match(false, ejabberd_auth:user_exists(<<"ibr_conflict_4">>, Server)),
608+
?match(true, mod_invites:is_token_valid(Server, RosterToken)),
609+
610+
mod_invites:remove_user(<<"inviter">>, Server),
611+
mod_invites:expire_tokens(<<>>, Server),
612+
?match(1, mod_invites:cleanup_expired()),
613+
disconnect(Config2),
614+
ok.
615+
541616
http(Config) ->
542617
Server = ?config(server, Config),
543618
User = ?config(user, Config),

0 commit comments

Comments
 (0)