Skip to content

Commit

Permalink
Resolver: don't use str.format (#11044)
Browse files Browse the repository at this point in the history
We were using .format over a user controlled string, the project prefix.
This can be used to traverse to some private variables in some cases
(this isn't the case here), or to cause a DoS by using an expensive
format operation (`{language:>999999999999}`).

This isn't a security issue, since we don't expose the prefix to users yet.
  • Loading branch information
stsewd authored Jan 22, 2024
1 parent 3cfc29b commit 7bb9530
Show file tree
Hide file tree
Showing 2 changed files with 14 additions and 13 deletions.
20 changes: 7 additions & 13 deletions readthedocs/core/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,13 @@ def base_resolve_path(
"""
Build a path using the given fields.
We first build a format string based on the given fields,
then we just call ``string.format()`` with the given values.
For example, if custom prefix is given, the path will be prefixed with it.
In case of a subproject (project_relationship is given),
the path will be prefixed with the subproject prefix
(defaults to ``/projects/<subproject-slug>/``).
Then we add the filename, version_slug and language to the path
depending on the versioning scheme.
"""
path = "/"

Expand All @@ -88,19 +88,13 @@ def base_resolve_path(
path = unsafe_join_url_path(path, custom_prefix)

if versioning_scheme == SINGLE_VERSION_WITHOUT_TRANSLATIONS:
path = unsafe_join_url_path(path, "{filename}")
path = unsafe_join_url_path(path, filename)
elif versioning_scheme == MULTIPLE_VERSIONS_WITHOUT_TRANSLATIONS:
path = unsafe_join_url_path(path, "{version}/{filename}")
path = unsafe_join_url_path(path, f"{version_slug}/{filename}")
else:
path = unsafe_join_url_path(path, "{language}/{version}/{filename}")
path = unsafe_join_url_path(path, f"{language}/{version_slug}/{filename}")

subproject_alias = project_relationship.alias if project_relationship else ""
return path.format(
filename=filename,
version=version_slug,
language=language,
subproject=subproject_alias,
)
return path

def resolve_path(
self,
Expand Down
7 changes: 7 additions & 0 deletions readthedocs/rtd_tests/tests/test_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -1078,6 +1078,13 @@ def test_custom_prefix_in_subproject_and_custom_prefix_in_superproject(self):
url, "http://pip.readthedocs.io/s/sub/prefix/es/latest/api/index.html"
)

def test_format_injection(self):
self.pip.custom_prefix = "/prefix/{language}"
self.pip.save()
url = resolver.resolve(self.pip)
# THe {language} inside the prefix isn't evaluated.
self.assertEqual(url, "http://pip.readthedocs.io/prefix/{language}/en/latest/")

def test_get_project_domain(self):
domain = resolver.get_domain(self.pip)
self.assertEqual(domain, "http://pip.readthedocs.io")
Expand Down

0 comments on commit 7bb9530

Please sign in to comment.