Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restrict MSC4108 content-type to text/plain #17122

Merged
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
11 changes: 11 additions & 0 deletions rust/src/rendezvous/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,17 @@ impl RendezvousHandler {

let content_type: ContentType = headers.typed_get_required()?;

// Content-Type must be text/plain
if content_type != ContentType::text() {
return Err(SynapseError::new(
StatusCode::BAD_REQUEST,
"Content-Type must be text/plain".to_owned(),
"M_INVALID_PARAM",
None,
None,
));
}

Ok(content_type.into())
Copy link
Member

Choose a reason for hiding this comment

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

If we know the content type will always by text/plain, we can probably simplify the code in this function and Session a bit.

Happy to have that be done in a follow-up PR though.

}

Expand Down
79 changes: 67 additions & 12 deletions tests/rest/client/test_rendezvous.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,7 @@ def test_msc4108(self) -> None:
"POST",
msc4108_endpoint,
"foo=bar",
# This sets the content type to application/x-www-form-urlencoded
content_is_form=True,
content_type=b"text/plain",
access_token=None,
)
self.assertEqual(channel.code, 201)
Expand Down Expand Up @@ -150,9 +149,7 @@ def test_msc4108(self) -> None:
headers = dict(channel.headers.getAllRawHeaders())
self.assertEqual(headers[b"ETag"], [etag])
self.assertIn(b"Expires", headers)
self.assertEqual(
headers[b"Content-Type"], [b"application/x-www-form-urlencoded"]
)
self.assertEqual(headers[b"Content-Type"], [b"text/plain"])
self.assertEqual(headers[b"Access-Control-Allow-Origin"], [b"*"])
self.assertEqual(headers[b"Access-Control-Expose-Headers"], [b"etag"])
self.assertEqual(headers[b"Cache-Control"], [b"no-store"])
Expand All @@ -174,7 +171,7 @@ def test_msc4108(self) -> None:
"PUT",
session_endpoint,
"foo=baz",
content_is_form=True,
content_type=b"text/plain",
access_token=None,
custom_headers=[("If-Match", etag)],
)
Expand All @@ -189,7 +186,7 @@ def test_msc4108(self) -> None:
"PUT",
session_endpoint,
"bar=baz",
content_is_form=True,
content_type=b"text/plain",
access_token=None,
custom_headers=[("If-Match", old_etag)],
)
Expand Down Expand Up @@ -258,7 +255,7 @@ def test_msc4108_expiration(self) -> None:
"POST",
msc4108_endpoint,
"foo=bar",
content_is_form=True,
content_type=b"text/plain",
access_token=None,
)
self.assertEqual(channel.code, 201)
Expand Down Expand Up @@ -311,7 +308,7 @@ def test_msc4108_capacity(self) -> None:
"POST",
msc4108_endpoint,
"foo=bar",
content_is_form=True,
content_type=b"text/plain",
access_token=None,
)
self.assertEqual(channel.code, 201)
Expand All @@ -332,7 +329,7 @@ def test_msc4108_capacity(self) -> None:
"POST",
msc4108_endpoint,
"foo=bar",
content_is_form=True,
content_type=b"text/plain",
access_token=None,
)
self.assertEqual(channel.code, 201)
Expand Down Expand Up @@ -383,7 +380,7 @@ def test_msc4108_hard_capacity(self) -> None:
"POST",
msc4108_endpoint,
"foo=bar",
content_is_form=True,
content_type=b"text/plain",
access_token=None,
)
self.assertEqual(channel.code, 201)
Expand All @@ -406,7 +403,7 @@ def test_msc4108_hard_capacity(self) -> None:
"POST",
msc4108_endpoint,
"foo=bar",
content_is_form=True,
content_type=b"text/plain",
access_token=None,
)
self.assertEqual(channel.code, 201)
Expand All @@ -419,3 +416,61 @@ def test_msc4108_hard_capacity(self) -> None:
)

self.assertEqual(channel.code, 404)

@unittest.skip_unless(HAS_AUTHLIB, "requires authlib")
@override_config(
{
"disable_registration": True,
"experimental_features": {
"msc4108_enabled": True,
"msc3861": {
"enabled": True,
"issuer": "https://issuer",
"client_id": "client_id",
"client_auth_method": "client_secret_post",
"client_secret": "client_secret",
"admin_token": "admin_token_value",
},
},
}
)
def test_msc4108_content_type(self) -> None:
"""
Test that the content-type is restricted to text/plain.
"""
# We cannot post invalid content-type arbitrary data to the endpoint
channel = self.make_request(
"POST",
msc4108_endpoint,
"foo=bar",
content_is_form=True,
access_token=None,
)
self.assertEqual(channel.code, 400)
self.assertEqual(channel.json_body["errcode"], "M_INVALID_PARAM")

# Make a valid request
channel = self.make_request(
"POST",
msc4108_endpoint,
"foo=bar",
content_type=b"text/plain",
access_token=None,
)
self.assertEqual(channel.code, 201)
url = urlparse(channel.json_body["url"])
session_endpoint = url.path
headers = dict(channel.headers.getAllRawHeaders())
etag = headers[b"ETag"][0]

# We can't update the data with invalid content-type
channel = self.make_request(
"PUT",
session_endpoint,
"foo=baz",
content_is_form=True,
access_token=None,
custom_headers=[("If-Match", etag)],
)
self.assertEqual(channel.code, 400)
self.assertEqual(channel.json_body["errcode"], "M_INVALID_PARAM")
7 changes: 6 additions & 1 deletion tests/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,7 @@ def make_request(
request: Type[Request] = SynapseRequest,
shorthand: bool = True,
federation_auth_origin: Optional[bytes] = None,
content_type: Optional[bytes] = None,
content_is_form: bool = False,
await_result: bool = True,
custom_headers: Optional[Iterable[CustomHeaderType]] = None,
Expand All @@ -373,6 +374,8 @@ def make_request(
with the usual REST API path, if it doesn't contain it.
federation_auth_origin: if set to not-None, we will add a fake
Authorization header pretenting to be the given server name.
content_type: The content-type to use for the request. If not set then will default to
application/json unless content_is_form is true.
content_is_form: Whether the content is URL encoded form data. Adds the
'Content-Type': 'application/x-www-form-urlencoded' header.
await_result: whether to wait for the request to complete rendering. If true,
Expand Down Expand Up @@ -436,7 +439,9 @@ def make_request(
)

if content:
if content_is_form:
if content_type is not None:
req.requestHeaders.addRawHeader(b"Content-Type", content_type)
elif content_is_form:
req.requestHeaders.addRawHeader(
b"Content-Type", b"application/x-www-form-urlencoded"
)
Expand Down
5 changes: 5 additions & 0 deletions tests/unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,7 @@ def make_request(
request: Type[Request] = SynapseRequest,
shorthand: bool = True,
federation_auth_origin: Optional[bytes] = None,
content_type: Optional[bytes] = None,
content_is_form: bool = False,
await_result: bool = True,
custom_headers: Optional[Iterable[CustomHeaderType]] = None,
Expand All @@ -541,6 +542,9 @@ def make_request(
with the usual REST API path, if it doesn't contain it.
federation_auth_origin: if set to not-None, we will add a fake
Authorization header pretenting to be the given server name.

content_type: The content-type to use for the request. If not set then will default to
application/json unless content_is_form is true.
content_is_form: Whether the content is URL encoded form data. Adds the
'Content-Type': 'application/x-www-form-urlencoded' header.

Expand All @@ -566,6 +570,7 @@ def make_request(
request,
shorthand,
federation_auth_origin,
content_type,
content_is_form,
await_result,
custom_headers,
Expand Down
Loading