Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Allow denying or shadow banning registrations via the spam checker #8034

Merged
merged 5 commits into from
Aug 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/8034.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add support for shadow-banning users (ignoring any message send requests).
35 changes: 33 additions & 2 deletions synapse/events/spamcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@
# limitations under the License.

import inspect
from typing import Any, Dict, List
from typing import Any, Dict, List, Optional, Tuple

from synapse.spam_checker_api import SpamCheckerApi
from synapse.spam_checker_api import RegistrationBehaviour, SpamCheckerApi
from synapse.types import Collection

MYPY = False
if MYPY:
Expand Down Expand Up @@ -160,3 +161,33 @@ def check_username_for_spam(self, user_profile: Dict[str, str]) -> bool:
return True

return False

def check_registration_for_spam(
self,
email_threepid: Optional[dict],
username: Optional[str],
request_info: Collection[Tuple[str, str]],
) -> RegistrationBehaviour:
"""Checks if we should allow the given registration request.

Args:
email_threepid: The email threepid used for registering, if any
username: The request user name, if any
request_info: List of tuples of user agent and IP that
were used during the registration process.

Returns:
Enum for how the request should be handled
"""

for spam_checker in self.spam_checkers:
# For backwards compatibility, only run if the method exists on the
# spam checker
checker = getattr(spam_checker, "check_registration_for_spam", None)
if checker:
behaviour = checker(email_threepid, username, request_info)
assert isinstance(behaviour, RegistrationBehaviour)
if behaviour != RegistrationBehaviour.ALLOW:
return behaviour

return RegistrationBehaviour.ALLOW
8 changes: 8 additions & 0 deletions synapse/handlers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,14 @@ async def check_ui_auth(
# authentication flow.
await self.store.set_ui_auth_clientdict(sid, clientdict)

user_agent = request.requestHeaders.getRawHeaders(b"User-Agent", default=[b""])[
0
].decode("ascii", "surrogateescape")

await self.store.add_user_agent_ip_to_ui_auth_session(
session.session_id, user_agent, clientip
)

if not authdict:
raise InteractiveAuthIncompleteError(
session.session_id, self._auth_dict_for_flows(flows, session.session_id)
Expand Down
11 changes: 10 additions & 1 deletion synapse/handlers/cas_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class CasHandler:
"""

def __init__(self, hs):
self.hs = hs
self._hostname = hs.hostname
self._auth_handler = hs.get_auth_handler()
self._registration_handler = hs.get_registration_handler()
Expand Down Expand Up @@ -210,8 +211,16 @@ async def handle_ticket(

else:
if not registered_user_id:
# Pull out the user-agent and IP from the request.
user_agent = request.requestHeaders.getRawHeaders(
b"User-Agent", default=[b""]
)[0].decode("ascii", "surrogateescape")
ip_address = self.hs.get_ip_from_request(request)

registered_user_id = await self._registration_handler.register_user(
localpart=localpart, default_display_name=user_display_name
localpart=localpart,
default_display_name=user_display_name,
user_agent_ips=(user_agent, ip_address),
)

await self._auth_handler.complete_sso_login(
Expand Down
21 changes: 18 additions & 3 deletions synapse/handlers/oidc_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ class OidcHandler:
"""

def __init__(self, hs: "HomeServer"):
self.hs = hs
self._callback_url = hs.config.oidc_callback_url # type: str
self._scopes = hs.config.oidc_scopes # type: List[str]
self._client_auth = ClientAuth(
Expand Down Expand Up @@ -689,9 +690,17 @@ async def handle_oidc_callback(self, request: SynapseRequest) -> None:
self._render_error(request, "invalid_token", str(e))
return

# Pull out the user-agent and IP from the request.
user_agent = request.requestHeaders.getRawHeaders(b"User-Agent", default=[b""])[
0
].decode("ascii", "surrogateescape")
ip_address = self.hs.get_ip_from_request(request)

# Call the mapper to register/login the user
try:
user_id = await self._map_userinfo_to_user(userinfo, token)
user_id = await self._map_userinfo_to_user(
userinfo, token, user_agent, ip_address
)
except MappingException as e:
logger.exception("Could not map user")
self._render_error(request, "mapping_error", str(e))
Expand Down Expand Up @@ -828,7 +837,9 @@ def _verify_expiry(self, caveat: str) -> bool:
now = self._clock.time_msec()
return now < expiry

async def _map_userinfo_to_user(self, userinfo: UserInfo, token: Token) -> str:
async def _map_userinfo_to_user(
self, userinfo: UserInfo, token: Token, user_agent: str, ip_address: str
) -> str:
"""Maps a UserInfo object to a mxid.

UserInfo should have a claim that uniquely identifies users. This claim
Expand All @@ -843,6 +854,8 @@ async def _map_userinfo_to_user(self, userinfo: UserInfo, token: Token) -> str:
Args:
userinfo: an object representing the user
token: a dict with the tokens obtained from the provider
user_agent: The user agent of the client making the request.
ip_address: The IP address of the client making the request.

Raises:
MappingException: if there was an error while mapping some properties
Expand Down Expand Up @@ -899,7 +912,9 @@ async def _map_userinfo_to_user(self, userinfo: UserInfo, token: Token) -> str:
# It's the first time this user is logging in and the mapped mxid was
# not taken, register the user
registered_user_id = await self._registration_handler.register_user(
localpart=localpart, default_display_name=attributes["display_name"],
localpart=localpart,
default_display_name=attributes["display_name"],
user_agent_ips=(user_agent, ip_address),
)

await self._datastore.record_user_external_id(
Expand Down
26 changes: 24 additions & 2 deletions synapse/handlers/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
ReplicationPostRegisterActionsServlet,
ReplicationRegisterServlet,
)
from synapse.spam_checker_api import RegistrationBehaviour
from synapse.storage.state import StateFilter
from synapse.types import RoomAlias, UserID, create_requester

Expand All @@ -52,6 +53,8 @@ def __init__(self, hs):
self.macaroon_gen = hs.get_macaroon_generator()
self._server_notices_mxid = hs.config.server_notices_mxid

self.spam_checker = hs.get_spam_checker()

if hs.config.worker_app:
self._register_client = ReplicationRegisterServlet.make_client(hs)
self._register_device_client = RegisterDeviceReplicationServlet.make_client(
Expand Down Expand Up @@ -142,7 +145,7 @@ async def register_user(
address=None,
bind_emails=[],
by_admin=False,
shadow_banned=False,
user_agent_ips=None,
):
"""Registers a new client on the server.

Expand All @@ -160,14 +163,33 @@ async def register_user(
bind_emails (List[str]): list of emails to bind to this account.
by_admin (bool): True if this registration is being made via the
admin api, otherwise False.
shadow_banned (bool): Shadow-ban the created user.
user_agent_ips (List[(str, str)]): Tuples of IP addresses and user-agents used
during the registration process.
Returns:
str: user_id
Raises:
SynapseError if there was a problem registering.
"""
self.check_registration_ratelimit(address)

result = self.spam_checker.check_registration_for_spam(
threepid, localpart, user_agent_ips or [],
)

if result == RegistrationBehaviour.DENY:
logger.info(
"Blocked registration of %r", localpart,
)
# We return a 429 to make it not obvious that they've been
# denied.
raise SynapseError(429, "Rate limited")

shadow_banned = result == RegistrationBehaviour.SHADOW_BAN
if shadow_banned:
logger.info(
"Shadow banning registration of %r", localpart,
)

# do not check_auth_blocking if the call is coming through the Admin API
if not by_admin:
await self.auth.check_auth_blocking(threepid=threepid)
Expand Down
18 changes: 16 additions & 2 deletions synapse/handlers/saml_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class Saml2SessionData:

class SamlHandler:
def __init__(self, hs: "synapse.server.HomeServer"):
self.hs = hs
self._saml_client = Saml2Client(hs.config.saml2_sp_config)
self._auth = hs.get_auth()
self._auth_handler = hs.get_auth_handler()
Expand Down Expand Up @@ -133,8 +134,14 @@ async def handle_saml_response(self, request: SynapseRequest) -> None:
# the dict.
self.expire_sessions()

# Pull out the user-agent and IP from the request.
user_agent = request.requestHeaders.getRawHeaders(b"User-Agent", default=[b""])[
0
].decode("ascii", "surrogateescape")
ip_address = self.hs.get_ip_from_request(request)

user_id, current_session = await self._map_saml_response_to_user(
resp_bytes, relay_state
resp_bytes, relay_state, user_agent, ip_address
)

# Complete the interactive auth session or the login.
Expand All @@ -147,14 +154,20 @@ async def handle_saml_response(self, request: SynapseRequest) -> None:
await self._auth_handler.complete_sso_login(user_id, request, relay_state)

async def _map_saml_response_to_user(
self, resp_bytes: str, client_redirect_url: str
self,
resp_bytes: str,
client_redirect_url: str,
user_agent: str,
ip_address: str,
) -> Tuple[str, Optional[Saml2SessionData]]:
"""
Given a sample response, retrieve the cached session and user for it.

Args:
resp_bytes: The SAML response.
client_redirect_url: The redirect URL passed in by the client.
user_agent: The user agent of the client making the request.
ip_address: The IP address of the client making the request.

Returns:
Tuple of the user ID and SAML session associated with this response.
Expand Down Expand Up @@ -291,6 +304,7 @@ async def _map_saml_response_to_user(
localpart=localpart,
default_display_name=displayname,
bind_emails=emails,
user_agent_ips=(user_agent, ip_address),
)

await self._datastore.record_user_external_id(
Expand Down
5 changes: 5 additions & 0 deletions synapse/rest/client/v2_alpha/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -591,12 +591,17 @@ async def on_POST(self, request):
Codes.THREEPID_IN_USE,
)

entries = await self.store.get_user_agents_ips_to_ui_auth_session(
session_id
)

registered_user_id = await self.registration_handler.register_user(
localpart=desired_username,
password_hash=password_hash,
guest_access_token=guest_access_token,
threepid=threepid,
address=client_addr,
user_agent_ips=entries,
)
# Necessary due to auth checks prior to the threepid being
# written to the db
Expand Down
11 changes: 11 additions & 0 deletions synapse/spam_checker_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
from enum import Enum

from twisted.internet import defer

Expand All @@ -25,6 +26,16 @@
logger = logging.getLogger(__name__)


class RegistrationBehaviour(Enum):
"""
Enum to define whether a registration request should allowed, denied, or shadow-banned.
"""

ALLOW = "allow"
SHADOW_BAN = "shadow_ban"
DENY = "deny"


class SpamCheckerApi(object):
"""A proxy object that gets passed to spam checkers so they can get
access to rooms and other relevant information.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/* Copyright 2020 The Matrix.org Foundation C.I.C
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

-- A table of the IP address and user-agent used to complete each step of a
-- user-interactive authentication session.
CREATE TABLE IF NOT EXISTS ui_auth_sessions_ips(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a comment please

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

session_id TEXT NOT NULL,
ip TEXT NOT NULL,
user_agent TEXT NOT NULL,
UNIQUE (session_id, ip, user_agent),
FOREIGN KEY (session_id)
REFERENCES ui_auth_sessions (session_id)
);
39 changes: 38 additions & 1 deletion synapse/storage/databases/main/ui_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Any, Dict, Optional, Union
from typing import Any, Dict, List, Optional, Tuple, Union

import attr
from canonicaljson import json
Expand Down Expand Up @@ -258,6 +258,34 @@ async def get_ui_auth_session_data(

return serverdict.get(key, default)

async def add_user_agent_ip_to_ui_auth_session(
self, session_id: str, user_agent: str, ip: str,
):
"""Add the given user agent / IP to the tracking table
"""
await self.db_pool.simple_upsert(
table="ui_auth_sessions_ips",
keyvalues={"session_id": session_id, "user_agent": user_agent, "ip": ip},
values={},
desc="add_user_agent_ip_to_ui_auth_session",
)

async def get_user_agents_ips_to_ui_auth_session(
self, session_id: str,
) -> List[Tuple[str, str]]:
"""Get the given user agents / IPs used during the ui auth process

Returns:
List of user_agent/ip pairs
"""
rows = await self.db_pool.simple_select_list(
table="ui_auth_sessions_ips",
keyvalues={"session_id": session_id},
retcols=("user_agent", "ip"),
desc="get_user_agents_ips_to_ui_auth_session",
)
return [(row["user_agent"], row["ip"]) for row in rows]


class UIAuthStore(UIAuthWorkerStore):
def delete_old_ui_auth_sessions(self, expiration_time: int):
Expand All @@ -281,6 +309,15 @@ def _delete_old_ui_auth_sessions_txn(self, txn, expiration_time: int):
txn.execute(sql, [expiration_time])
session_ids = [r[0] for r in txn.fetchall()]

# Delete the corresponding IP/user agents.
self.db_pool.simple_delete_many_txn(
txn,
table="ui_auth_sessions_ips",
column="session_id",
iterable=session_ids,
keyvalues={},
)

# Delete the corresponding completed credentials.
self.db_pool.simple_delete_many_txn(
txn,
Expand Down
Loading