comparison libervia/web/server/server.py @ 1598:86c7a3a625d5

server: always start a new session on connection: The session was kept when a user was connecting from service profile (but not from other profiles), this was leading to session fixation vulnerability (an attacker on the same machine could get service profile session cookie, and use it when a victim would log-in). This patch fixes it by always starting a new session on connection. fix 443
author Goffi <goffi@goffi.org>
date Fri, 23 Feb 2024 13:35:24 +0100
parents 93abef9a3548
children d07838fc9d99
comparison
equal deleted inserted replaced
1597:c1c1d68d063e 1598:86c7a3a625d5
829 web_session.backend_started = int(infos["started"]) 829 web_session.backend_started = int(infos["started"])
830 830
831 state = C.PROFILE_LOGGED_EXT_JID if register_with_ext_jid else C.PROFILE_LOGGED 831 state = C.PROFILE_LOGGED_EXT_JID if register_with_ext_jid else C.PROFILE_LOGGED
832 return state 832 return state
833 833
834 @defer.inlineCallbacks 834 async def connect(self, request, login, password):
835 def connect(self, request, login, password):
836 """log user in 835 """log user in
837 836
838 If an other user was already logged, it will be unlogged first 837 If an other user was already logged, it will be unlogged first
839 @param request(server.Request): request linked to the session 838 @param request(server.Request): request linked to the session
840 @param login(unicode): user login 839 @param login(unicode): user login
852 @raise exceptions.TimeoutError: didn't received and answer from bridge 851 @raise exceptions.TimeoutError: didn't received and answer from bridge
853 @raise exceptions.InternalError: unknown error 852 @raise exceptions.InternalError: unknown error
854 @raise ValueError(C.PROFILE_AUTH_ERROR): invalid login and/or password 853 @raise ValueError(C.PROFILE_AUTH_ERROR): invalid login and/or password
855 @raise ValueError(C.XMPP_AUTH_ERROR): invalid XMPP account password 854 @raise ValueError(C.XMPP_AUTH_ERROR): invalid XMPP account password
856 """ 855 """
857
858 # XXX: all security checks must be done here, even if present in javascript 856 # XXX: all security checks must be done here, even if present in javascript
859 if login.startswith("@"): 857 if login.startswith("@"):
860 raise failure.Failure(exceptions.DataError("No profile_key allowed")) 858 raise failure.Failure(exceptions.DataError("No profile_key allowed"))
861 859
862 if login.startswith("guest@@") and login.count("@") == 2: 860 if login.startswith("guest@@") and login.count("@") == 2:
870 login_jid = jid.JID(login) 868 login_jid = jid.JID(login)
871 except (RuntimeError, jid.InvalidFormat, AttributeError): 869 except (RuntimeError, jid.InvalidFormat, AttributeError):
872 raise failure.Failure(exceptions.DataError("No profile_key allowed")) 870 raise failure.Failure(exceptions.DataError("No profile_key allowed"))
873 871
874 # FIXME: should it be cached? 872 # FIXME: should it be cached?
875 new_account_domain = yield self.bridge_call("account_domain_new_get") 873 new_account_domain = await self.bridge_call("account_domain_new_get")
876 874
877 if login_jid.host == new_account_domain: 875 if login_jid.host == new_account_domain:
878 # redirect "user@libervia.org" to the "user" profile 876 # redirect "user@libervia.org" to the "user" profile
879 login = login_jid.user 877 login = login_jid.user
880 login_jid = None 878 login_jid = None
881 else: 879 else:
882 login_jid = None 880 login_jid = None
883 881
884 try: 882 try:
885 profile = yield self.bridge_call("profile_name_get", login) 883 profile = await self.bridge_call("profile_name_get", login)
886 except Exception: # XXX: ProfileUnknownError wouldn't work, it's encapsulated 884 except Exception: # XXX: ProfileUnknownError wouldn't work, it's encapsulated
887 # FIXME: find a better way to handle bridge errors 885 # FIXME: find a better way to handle bridge errors
888 if ( 886 if (
889 login_jid is not None and login_jid.user 887 login_jid is not None and login_jid.user
890 ): # try to create a new libervia.backend profile using the XMPP credentials 888 ): # try to create a new libervia.backend profile using the XMPP credentials
912 raise exceptions.PermissionError 910 raise exceptions.PermissionError
913 register_with_ext_jid = False 911 register_with_ext_jid = False
914 912
915 connect_method = "connect" 913 connect_method = "connect"
916 914
917 # we check if there is not already an active session 915 # we check the active session
918 web_session = session_iface.IWebSession(request.getSession()) 916 web_session = session_iface.IWebSession(request.getSession())
919 if web_session.profile: 917 if web_session.profile != profile:
920 # yes, there is 918 # It's a different profile, we need to disconnect it.
921 if web_session.profile != profile: 919 # We always purge session even if no user was logged, to avoid the re-use of
922 # it's a different profile, we need to disconnect it 920 # cookie (i.e. to avoid session fixation).
921 if web_session.profile:
922 # no need to log a warning if the previous profile was the service profile
923 log.warning(_( 923 log.warning(_(
924 "{new_profile} requested login, but {old_profile} was already " 924 "{new_profile} requested login, but {old_profile} was already "
925 "connected, disconnecting {old_profile}").format( 925 "connected, disconnecting {old_profile}").format(
926 old_profile=web_session.profile, new_profile=profile)) 926 old_profile=web_session.profile, new_profile=profile))
927 self.purge_session(request) 927 self.purge_session(request)
928 928
929 if self.waiting_profiles.get_request(profile): 929 if self.waiting_profiles.get_request(profile):
930 #  FIXME: check if and when this can happen 930 #  FIXME: check if and when this can happen
931 raise failure.Failure(exceptions.NotReady("Already waiting")) 931 raise failure.Failure(exceptions.NotReady("Already waiting"))
932 932
933 self.waiting_profiles.set_request(request, profile, register_with_ext_jid) 933 self.waiting_profiles.set_request(request, profile, register_with_ext_jid)
934 try: 934 try:
935 connected = yield self.bridge_call(connect_method, profile, password) 935 connected = await self.bridge_call(connect_method, profile, password)
936 except Exception as failure_: 936 except Exception as failure_:
937 fault = getattr(failure_, 'classname', None) 937 fault = getattr(failure_, 'classname', None)
938 self.waiting_profiles.purge_request(profile) 938 self.waiting_profiles.purge_request(profile)
939 if fault in ("PasswordError", "ProfileUnknownError"): 939 if fault in ("PasswordError", "ProfileUnknownError"):
940 log.info("Profile {profile} doesn't exist or the submitted password is " 940 log.info("Profile {profile} doesn't exist or the submitted password is "
969 log.error(_( 969 log.error(_(
970 "session profile [{session_profile}] differs from login " 970 "session profile [{session_profile}] differs from login "
971 "profile [{profile}], this should not happen!") 971 "profile [{profile}], this should not happen!")
972 .format(session_profile=web_session.profile, profile=profile)) 972 .format(session_profile=web_session.profile, profile=profile))
973 raise exceptions.InternalError("profile mismatch") 973 raise exceptions.InternalError("profile mismatch")
974 defer.returnValue(C.SESSION_ACTIVE) 974 return C.SESSION_ACTIVE
975 log.info( 975 log.info(
976 _( 976 _(
977 "profile {profile} was already connected in backend".format( 977 "profile {profile} was already connected in backend".format(
978 profile=profile 978 profile=profile
979 ) 979 )
980 ) 980 )
981 ) 981 )
982 #  no, we have to create it 982 #  no, we have to create it
983 983
984 state = yield defer.ensureDeferred(self._logged(profile, request)) 984 state = await self._logged(profile, request)
985 defer.returnValue(state) 985 return state
986 986
987 async def check_registration_id(self, request: server.Request) -> None: 987 async def check_registration_id(self, request: server.Request) -> None:
988 """Check if a valid registration ID is found in request data 988 """Check if a valid registration ID is found in request data
989 989
990 @param request: request used for account registration 990 @param request: request used for account registration