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

Add validation of format for 3pid and add validation of 3pid in admin api #7022

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from 7 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/6398.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add validation of format for 3pid.
Copy link
Member

Choose a reason for hiding this comment

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

Contrary to how towncrier is normally used, we've been putting in newsfragments just for the pull request number, so this file can be removed.

Copy link
Contributor Author

@dklimpel dklimpel Mar 9, 2020

Choose a reason for hiding this comment

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

Ok.

Copy link
Member

Choose a reason for hiding this comment

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

Please delete this file.

1 change: 1 addition & 0 deletions changelog.d/7022.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add validation of 3pid in admin api ``PUT /_synapse/admin/v2/users/<user_id>``.
1 change: 1 addition & 0 deletions synapse/api/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class Codes(object):
THREEPID_IN_USE = "M_THREEPID_IN_USE"
THREEPID_NOT_FOUND = "M_THREEPID_NOT_FOUND"
THREEPID_DENIED = "M_THREEPID_DENIED"
INVALID_THREEPID = "M_INVALID_THREEPID"
Copy link
Member

Choose a reason for hiding this comment

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

It is unclear to me if this needs a spec change or not. @matrix-org/synapse-core any opinion?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did not find an spec for these errors. Only a documenttion https://github.com/matrix-org/matrix-doc/blob/master/specification/client_server_api.rst
I can also remove this error.

Copy link
Member

Choose a reason for hiding this comment

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

You can see the list of error codes at https://matrix.org/docs/spec/client_server/r0.6.0#api-standards, this would need to get added as a separate one following the process at https://matrix.org/docs/spec/proposals

INVALID_USERNAME = "M_INVALID_USERNAME"
SERVER_NOT_TRUSTED = "M_SERVER_NOT_TRUSTED"
CONSENT_NOT_GIVEN = "M_CONSENT_NOT_GIVEN"
Expand Down
59 changes: 57 additions & 2 deletions synapse/rest/admin/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
historical_admin_path_patterns,
)
from synapse.types import UserID
from synapse.util.threepids import check_3pid_allowed, check_3pid_valid_format

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -195,8 +196,35 @@ async def on_PUT(self, request, user_id):
# add new threepids to user
current_time = self.hs.get_clock().time_msec()
for threepid in body["threepids"]:
# For emails, transform the address to lowercase.
# We store all email addreses as lowercase in the DB.
# (See add_threepid in synapse/handlers/auth.py)
address = threepid["address"].lower()
Copy link
Member

Choose a reason for hiding this comment

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

Just noting that this seems related to #7021.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, it is.

Copy link
Member

Choose a reason for hiding this comment

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

This change needs to wait on the result of that conversation then.

if not check_3pid_valid_format(threepid["medium"], address):
raise SynapseError(
400,
"Third party identifier has not a valid format",
Codes.INVALID_THREEPID,
)

if not check_3pid_allowed(self.hs, threepid["medium"], address):
raise SynapseError(
403,
"Your email domain or account phone number is not authorized on this server",
Codes.THREEPID_DENIED,
)

existing_user_id = await self.store.get_user_id_by_threepid(
threepid["medium"], address
)

if existing_user_id is not None:
raise SynapseError(
400, "Threepid is already in use", Codes.THREEPID_IN_USE
)

await self.auth_handler.add_threepid(
user_id, threepid["medium"], threepid["address"], current_time
user_id, threepid["medium"], address, current_time
)

if "avatar_url" in body:
Expand Down Expand Up @@ -271,8 +299,35 @@ async def on_PUT(self, request, user_id):

current_time = self.hs.get_clock().time_msec()
for threepid in body["threepids"]:
# For emails, transform the address to lowercase.
# We store all email addreses as lowercase in the DB.
# (See add_threepid in synapse/handlers/auth.py)
address = threepid["address"].lower()
if not check_3pid_valid_format(threepid["medium"], address):
raise SynapseError(
400,
"Third party identifier has not a valid format",
dklimpel marked this conversation as resolved.
Show resolved Hide resolved
Codes.INVALID_THREEPID,
)

if not check_3pid_allowed(self.hs, threepid["medium"], address):
raise SynapseError(
403,
"Your email domain or account phone number is not authorized on this server",
Codes.THREEPID_DENIED,
)

existing_user_id = await self.store.get_user_id_by_threepid(
threepid["medium"], address
)

if existing_user_id is not None:
raise SynapseError(
400, "Threepid is already in use", Codes.THREEPID_IN_USE
)

await self.auth_handler.add_threepid(
user_id, threepid["medium"], threepid["address"], current_time
user_id, threepid["medium"], address, current_time
)

if "avatar_url" in body:
Expand Down
23 changes: 22 additions & 1 deletion synapse/rest/client/v2_alpha/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
from synapse.push.mailer import Mailer, load_jinja2_templates
from synapse.util.msisdn import phone_number_to_msisdn
from synapse.util.stringutils import assert_valid_client_secret
from synapse.util.threepids import check_3pid_allowed
from synapse.util.threepids import check_3pid_allowed, check_3pid_valid_format

from ._base import client_patterns, interactive_auth_handler

Expand Down Expand Up @@ -88,6 +88,13 @@ async def on_POST(self, request):
send_attempt = body["send_attempt"]
next_link = body.get("next_link") # Optional param

if not check_3pid_valid_format("email", email):
raise SynapseError(
400,
"Third party identifier has not a valid format",
Codes.INVALID_THREEPID,
)

if not check_3pid_allowed(self.hs, "email", email):
raise SynapseError(
403,
Expand Down Expand Up @@ -363,6 +370,13 @@ async def on_POST(self, request):
send_attempt = body["send_attempt"]
next_link = body.get("next_link") # Optional param

if not check_3pid_valid_format("email", email):
raise SynapseError(
400,
"Third party identifier has not a valid format",
Codes.INVALID_THREEPID,
)

if not check_3pid_allowed(self.hs, "email", email):
raise SynapseError(
403,
Expand Down Expand Up @@ -428,6 +442,13 @@ async def on_POST(self, request):

msisdn = phone_number_to_msisdn(country, phone_number)

if not check_3pid_valid_format("msisdn", msisdn):
raise SynapseError(
400,
"Third party identifier has not a valid format",
Codes.INVALID_THREEPID,
)

if not check_3pid_allowed(self.hs, "msisdn", msisdn):
raise SynapseError(
403,
Expand Down
23 changes: 22 additions & 1 deletion synapse/rest/client/v2_alpha/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
from synapse.util.msisdn import phone_number_to_msisdn
from synapse.util.ratelimitutils import FederationRateLimiter
from synapse.util.stringutils import assert_valid_client_secret
from synapse.util.threepids import check_3pid_allowed
from synapse.util.threepids import check_3pid_allowed, check_3pid_valid_format

from ._base import client_patterns, interactive_auth_handler

Expand Down Expand Up @@ -123,6 +123,13 @@ async def on_POST(self, request):
send_attempt = body["send_attempt"]
next_link = body.get("next_link") # Optional param

if not check_3pid_valid_format("email", email):
raise SynapseError(
400,
"Third party identifier has not a valid format",
Codes.INVALID_THREEPID,
)

if not check_3pid_allowed(self.hs, "email", email):
raise SynapseError(
403,
Expand Down Expand Up @@ -190,6 +197,13 @@ async def on_POST(self, request):

msisdn = phone_number_to_msisdn(country, phone_number)

if not check_3pid_valid_format("msisdn", msisdn):
raise SynapseError(
400,
"Third party identifier has not a valid format",
Codes.INVALID_THREEPID,
)

if not check_3pid_allowed(self.hs, "msisdn", msisdn):
raise SynapseError(
403,
Expand Down Expand Up @@ -514,6 +528,13 @@ async def on_POST(self, request):
medium = auth_result[login_type]["medium"]
address = auth_result[login_type]["address"]

if not check_3pid_valid_format(medium, address):
raise SynapseError(
400,
"Third party identifier has not a valid format",
Codes.INVALID_THREEPID,
)

if not check_3pid_allowed(self.hs, medium, address):
raise SynapseError(
403,
Expand Down
22 changes: 22 additions & 0 deletions synapse/util/threepids.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,25 @@ def check_3pid_allowed(hs, medium, address):
return True

return False


def check_3pid_valid_format(medium, address):
"""Checks whether 3pid has a valid format

Args:
medium (str): 3pid medium - e.g. email, msisdn
address (str): 3pid address to check (e.g. "wotan@matrix.org")
dklimpel marked this conversation as resolved.
Show resolved Hide resolved
Returns:
bool: whether the email address has a valid format
"""

if medium == "email":
regex = r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)"
Copy link
Member

Choose a reason for hiding this comment

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

At a glance this looks like it'll hit most valid e-mails, but usually things like e-mails and domains are relatively hard to validate -- where did this regular expression come from?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Source of regex is: https://emailregex.com/

Python
r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$)"

We can change the regex. That is the reason why the validation is a new function.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did not found a good regex for msidns.

Copy link
Member

Choose a reason for hiding this comment

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

The main issue I see here is that this doesn't support international domain names (or Unicode in general, although I'm not sure if Unicode is valid in the local part of an email address).

Taking a look at that site, it seems the Python regular expression is much simpler than some of the others, which I find concerning.

Poking around a bit, some other solutions seem to suggest being generous since it is hard to know the form of an email without trying to actually validate it and use something like: r"^[^@]+@[^@]+\.[^@]+$".

if re.search(regex, address):
return True
else:
return False
dklimpel marked this conversation as resolved.
Show resolved Hide resolved
elif medium == "msisdn":
return True
else:
return False
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 above this saying that any other medium is not understood and is thus invalid? Thanks!

81 changes: 79 additions & 2 deletions tests/rest/admin/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,83 @@ def test_set_threepid(self):
self.assertEqual("email", channel.json_body["threepids"][0]["medium"])
self.assertEqual("bob3@bob.bob", channel.json_body["threepids"][0]["address"])

def test_set_duplicate_threepid(self):
"""
Test setting duplicate threepid for different user.
"""
self.hs.config.registration_shared_secret = None

# Add duplicate threepid with different notations
body = json.dumps(
{
"threepids": [
{"medium": "email", "address": "bob3@bob.bob"},
{"medium": "email", "address": "BOB3@bob.BOB"},
]
}
)

request, channel = self.make_request(
"PUT",
self.url_other_user,
access_token=self.admin_user_tok,
content=body.encode(encoding="utf_8"),
)
self.render(request)

self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])
self.assertEqual("Threepid is already in use", channel.json_body["error"])

def test_set_invalid_threepid(self):
"""
Test setting invalid threepid for an other user.
"""
self.hs.config.registration_shared_secret = None

# Add threepid to user
body = json.dumps({"threepids": [{"medium": "email", "address": "bob3@bob"}]})

request, channel = self.make_request(
"PUT",
self.url_other_user,
access_token=self.admin_user_tok,
content=body.encode(encoding="utf_8"),
)
self.render(request)

self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])
self.assertEqual(
"Third party identifier has not a valid format", channel.json_body["error"]
)

def test_set_not_allowed_threepid(self):
"""
Test setting not allowed threepid for an other user.
"""
self.hs.config.registration_shared_secret = None
self.hs.config.allowed_local_3pids = [
{"medium": "email", "pattern": r".*@matrix\.org"}
]

# Add threepid to user
body = json.dumps(
{"threepids": [{"medium": "email", "address": "bob3@bob.bob"}]}
)

request, channel = self.make_request(
"PUT",
self.url_other_user,
access_token=self.admin_user_tok,
content=body.encode(encoding="utf_8"),
)
self.render(request)

self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"])
self.assertEqual(
"Your email domain or account phone number is not authorized on this server",
channel.json_body["error"],
)

def test_deactivate_user(self):
"""
Test deactivating another user.
Expand Down Expand Up @@ -693,7 +770,7 @@ def test_accidental_deactivation_prevention(self):
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
self.assertEqual("@bob:test", channel.json_body["name"])
self.assertEqual("bob", channel.json_body["displayname"])
self.assertEqual(0, channel.json_body["deactivated"])
self.assertEqual(False, channel.json_body["deactivated"])

# Change password (and use a str for deactivate instead of a bool)
body = json.dumps({"password": "abc123", "deactivated": "false"}) # oops!
Expand All @@ -719,4 +796,4 @@ def test_accidental_deactivation_prevention(self):
self.assertEqual("bob", channel.json_body["displayname"])

# Ensure they're still alive
self.assertEqual(0, channel.json_body["deactivated"])
self.assertEqual(False, channel.json_body["deactivated"])