Repository: marshmallow-code/marshmallow

Issue:
Incorrect Email Validation
https://github.com/marshmallow-code/marshmallow/blob/fbe22eb47db5df64b2c4133f9a5cb6c6920e8dd2/src/marshmallow/validate.py#L136-L151

The email validation regex will match `email@domain.com\n`, `email\n@domain.com`, and `email\n@domain.com\n`.

The issue is that `$` is used to match until the end of a string. Instead, `\Z` should be used. - https://stackoverflow.com/a/48730645

It is possible that other validators might suffer from the same bug, so it would be good if other regexes were also checked.

It is unclear, but this may lead to a security vulnerability in some projects that use marshmallow (depending on how the validator is used), so a quick fix here might be helpful. In my quick look around I didn't notice anything critical, however, so I figured it would be fine to open this issue.


Discussion:
`^` behaves as expected, but `$` also matches a "string-ending newline". This appears to be a holdover from POSIX that most languages have long abandoned as default behavior.

Thanks for reporting this @nbanmp. If you are interesting in contributing a MR, I would be happy to review it, otherwise I can pick this up as soon as I get a chance.

Also, can you provide any examples of using an email address in a way that would make this issue a security vulnerability? The best I can come up with is that it will terminate the email headers early and cause an email to be malformed or undeliverable, which would be a standard bug. Since the newline can only be at the very end of the string, this would not allow injecting additional headers into an email. If it does have security implications I can submit a CVE and flag the vulnerable versions on TideLift.
I don't know if I would call this a vulnerability in marshmallow, but it might - in rare cases - allow for exploitation of certain vulnerabilities in other tools.

The most likely place this might result in a vulnerability is when verifying an email for authentication, and allowing multiple accounts with the same, but different emails.

A minor form of this would be allowing the same email to be used multiple times in sites that want to prevent that.

A more serious, but unlikely form of this would be something like:

Depending on how emails are stored / used, whitespace around them might be stripped or not between sending emails, and checking emails. It's not a vulnerability in marshmallow itself, but it might result in inconsistencies in other apps lead to actual vulnerabilities.

```python
validateemail(email)

if email in database:
    error()
else:
    saveaccount(email.strip(), password) # This might overwrite an account with the correct email
```

This example is actually what I was thinking of, because I was looking through the code for CTFd, which had a similar vulnerability occurring with the username, and, if not for dumb luck (a random lack of .strip() / it being in a different place), the same vulnerability would have also been possible using the email, even though the email was invalid.


Patch:
diff --git a/src/marshmallow/validate.py b/src/marshmallow/validate.py
--- a/src/marshmallow/validate.py
+++ b/src/marshmallow/validate.py
@@ -65,7 +65,7 @@ def _regex_generator(self, relative, require_tld):
                 r'\[?[A-F0-9]*:[A-F0-9:]+\]?)',  # ...or ipv6
                 r'(?::\d+)?',  # optional port
                 r')?' if relative else r'',  # host is optional, allow for relative URLs
-                r'(?:/?|[/?]\S+)$',
+                r'(?:/?|[/?]\S+)\Z',
             )), re.IGNORECASE)
 
         def __call__(self, relative, require_tld):
@@ -120,18 +120,18 @@ class Email(Validator):
     """
 
     USER_REGEX = re.compile(
-        r"(^[-!#$%&'*+/=?^`{}|~\w]+(\.[-!#$%&'*+/=?^`{}|~\w]+)*$"  # dot-atom
+        r"(^[-!#$%&'*+/=?^`{}|~\w]+(\.[-!#$%&'*+/=?^`{}|~\w]+)*\Z"  # dot-atom
         # quoted-string
         r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]'
-        r'|\\[\001-\011\013\014\016-\177])*"$)', re.IGNORECASE | re.UNICODE)
+        r'|\\[\001-\011\013\014\016-\177])*"\Z)', re.IGNORECASE | re.UNICODE)
 
     DOMAIN_REGEX = re.compile(
         # domain
         r'(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+'
-        r'(?:[A-Z]{2,6}|[A-Z0-9-]{2,})$'
+        r'(?:[A-Z]{2,6}|[A-Z0-9-]{2,})\Z'
         # literal form, ipv4 address (SMTP 4.1.3)
         r'|^\[(25[0-5]|2[0-4]\d|[0-1]?\d?\d)'
-        r'(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\]$', re.IGNORECASE | re.UNICODE)
+        r'(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\]\Z', re.IGNORECASE | re.UNICODE)
 
     DOMAIN_WHITELIST = ('localhost',)
 


Test patch:
diff --git a/tests/test_validate.py b/tests/test_validate.py
--- a/tests/test_validate.py
+++ b/tests/test_validate.py
@@ -41,6 +41,7 @@ def test_url_absolute_valid(valid_url):
     'http:/example.org',
     'foo://example.org',
     '../icons/logo.gif',
+    'https://example.org\n',
     'abc',
     '..',
     '/',
@@ -71,6 +72,7 @@ def test_url_relative_valid(valid_url):
     'suppliers.html',
     '../icons/logo.gif',
     'icons/logo.gif',
+    'http://example.org\n',
     '../.../g',
     '...',
     '\\',
@@ -98,6 +100,7 @@ def test_url_dont_require_tld_valid(valid_url):
 
 @pytest.mark.parametrize('invalid_url', [
     'http//example',
+    'http://example\n',
     'http://.example.org',
     'http:///foo/bar',
     'http:// /foo/bar',
@@ -165,6 +168,8 @@ def test_email_valid(valid_email):
     assert validator(valid_email) == valid_email
 
 @pytest.mark.parametrize('invalid_email', [
+    'niceandsimple\n@example.com',
+    'NiCeAnDsImPlE@eXaMpLe.CoM\n',
     'a"b(c)d,e:f;g<h>i[j\\k]l@example.com',
     'just"not"right@example.com',
     'this is"not\allowed@example.com',