Mercurial > libervia-web
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 |