Skip to content
Open
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
4 changes: 3 additions & 1 deletion binderhub/repoproviders.py
Original file line number Diff line number Diff line change
Expand Up @@ -1054,11 +1054,13 @@ async def get_resolved_ref(self):
if hasattr(self, "resolved_ref"):
return self.resolved_ref

# Encode the ref (safe="" so its slashes can't inject extra path
# segments) to prevent traversal, see GHSA-q276-fxp7-xhx3.
api_url = "{api_base_path}/repos/{user}/{repo}/commits/{ref}".format(
api_base_path=self.api_base_path.format(hostname=self.hostname),
user=self.user,
repo=self.repo,
ref=self.unresolved_ref,
ref=urllib.parse.quote(self.unresolved_ref, safe=""),
)
self.log.debug("Fetching %s", api_url)
cached = self.cache.get(api_url)
Expand Down
22 changes: 21 additions & 1 deletion binderhub/tests/test_repoproviders.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import re
from unittest import TestCase
from urllib.parse import quote
from urllib.parse import quote, urlparse

import pytest
from tornado.ioloop import IOLoop
Expand Down Expand Up @@ -417,6 +417,26 @@ def test_github_missing_ref():
assert ref is None


async def test_github_ref_is_url_encoded(monkeypatch):
"""An attacker-controlled ref must not inject extra GitHub API path segments."""
ref = "HEAD/../../../../owner/private/contents/README.md"
provider = GitHubRepoProvider(spec=f"user/repo/{ref}")

captured = {}

async def fake_request(api_url, etag=None):
captured["url"] = api_url
return None # short-circuit before any network call

monkeypatch.setattr(provider, "github_api_request", fake_request)
await provider.get_resolved_ref()

path = urlparse(captured["url"]).path
# the ref's slashes are encoded, so the ".." cannot climb out of /commits/
assert path == f"/repos/user/repo/commits/{quote(ref, safe='')}"
assert "/../" not in path


class TestSpecErrorHandling(TestCase):
def test_too_short_spec(self):
spec = "nothing_to_split"
Expand Down
Loading