From 7bb977bf38a1a7308d4941a8d54e796e4bc8f8db Mon Sep 17 00:00:00 2001 From: Anas Khan <83116240+anxkhn@users.noreply.github.com> Date: Mon, 29 Jun 2026 12:58:54 +0530 Subject: [PATCH] fix(python-sdk): percent-encode git credentials in with_credentials git.clone and git.pull embed HTTP(S) credentials via with_credentials, which interpolated the username and password straight into the URL netloc. A user/token containing URL-reserved characters (@ : / # ?) produced a corrupted git URL and broke authentication. Percent-encode both parts with quote(safe=''), matching the JS SDK which already encodes via the WHATWG URL setters. Alphanumeric tokens are unchanged. Add offline unit tests for the Python helper and a JS parity test to lock the encoded output. --- .changeset/python-git-credentials-encode.md | 12 +++++ .../tests/sandbox/git/validation.test.ts | 16 +++++++ packages/python-sdk/e2b/sandbox/_git/auth.py | 4 +- .../python-sdk/tests/shared/git/test_auth.py | 46 +++++++++++++++++++ 4 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 .changeset/python-git-credentials-encode.md create mode 100644 packages/python-sdk/tests/shared/git/test_auth.py diff --git a/.changeset/python-git-credentials-encode.md b/.changeset/python-git-credentials-encode.md new file mode 100644 index 0000000000..16eb7371fc --- /dev/null +++ b/.changeset/python-git-credentials-encode.md @@ -0,0 +1,12 @@ +--- +"@e2b/python-sdk": patch +--- + +fix(python-sdk): percent-encode git credentials so reserved characters are preserved + +`Sandbox.git.clone`/`push` embed credentials into HTTP(S) URLs with +`with_credentials`, which previously interpolated the username and password +straight into the URL. A username or token containing URL-reserved characters +(`@ : / # ?`) produced a corrupted URL and broke git auth. The credentials are +now percent-encoded, matching the JavaScript SDK, which already encodes via the +WHATWG `URL` setters. Alphanumeric tokens are unaffected. diff --git a/packages/js-sdk/tests/sandbox/git/validation.test.ts b/packages/js-sdk/tests/sandbox/git/validation.test.ts index 24bd2a9190..54a50e931e 100644 --- a/packages/js-sdk/tests/sandbox/git/validation.test.ts +++ b/packages/js-sdk/tests/sandbox/git/validation.test.ts @@ -3,6 +3,7 @@ import { test, expect } from 'vitest' import { Git } from '../../../src/sandbox/git' import type { Commands } from '../../../src/sandbox/commands' import { InvalidArgumentError } from '../../../src/errors' +import { withCredentials } from '../../../src/sandbox/git/utils' // Stub command runner that fails if a git command is actually executed — // validation must throw before reaching it. @@ -42,3 +43,18 @@ test('git.remoteGet throws InvalidArgumentError when name is missing', async () const git = new Git(failingCommands) await expect(git.remoteGet('/repo', '')).rejects.toThrow(InvalidArgumentError) }) + +test('withCredentials percent-encodes reserved characters', () => { + expect( + withCredentials('https://github.com/o/r.git', 'user', 'p@ss/w:rd') + ).toBe('https://user:p%40ss%2Fw%3Ard@github.com/o/r.git') + expect(withCredentials('https://github.com/o/r.git', 'user', 'x#y?z')).toBe( + 'https://user:x%23y%3Fz@github.com/o/r.git' + ) + expect(withCredentials('https://github.com/o/r.git', 'git', 't/k@n')).toBe( + 'https://git:t%2Fk%40n@github.com/o/r.git' + ) + expect( + withCredentials('https://github.com/o/r.git', 'user', 'ghp_AbC123') + ).toBe('https://user:ghp_AbC123@github.com/o/r.git') +}) diff --git a/packages/python-sdk/e2b/sandbox/_git/auth.py b/packages/python-sdk/e2b/sandbox/_git/auth.py index a93511f48d..c33f7e80a7 100644 --- a/packages/python-sdk/e2b/sandbox/_git/auth.py +++ b/packages/python-sdk/e2b/sandbox/_git/auth.py @@ -1,5 +1,5 @@ from typing import Optional -from urllib.parse import urlparse, urlunparse +from urllib.parse import quote, urlparse, urlunparse from e2b.exceptions import InvalidArgumentException from e2b.sandbox.commands.command_handle import CommandExitException @@ -27,7 +27,7 @@ def with_credentials(url: str, username: Optional[str], password: Optional[str]) "Only http(s) Git URLs support username/password credentials." ) - netloc = f"{username}:{password}@{parsed.netloc}" + netloc = f"{quote(username, safe='')}:{quote(password, safe='')}@{parsed.netloc}" return urlunparse(parsed._replace(netloc=netloc)) diff --git a/packages/python-sdk/tests/shared/git/test_auth.py b/packages/python-sdk/tests/shared/git/test_auth.py new file mode 100644 index 0000000000..ecb9ecb513 --- /dev/null +++ b/packages/python-sdk/tests/shared/git/test_auth.py @@ -0,0 +1,46 @@ +import pytest + +from e2b.exceptions import InvalidArgumentException +from e2b.sandbox._git import with_credentials + + +def test_with_credentials_percent_encodes_reserved_characters(): + # Reserved characters in user/password must be percent-encoded so the + # credential URL stays well-formed (parity with the JS SDK's WHATWG URL). + assert ( + with_credentials("https://github.com/o/r.git", "user", "p@ss/w:rd") + == "https://user:p%40ss%2Fw%3Ard@github.com/o/r.git" + ) + assert ( + with_credentials("https://github.com/o/r.git", "user", "x#y?z") + == "https://user:x%23y%3Fz@github.com/o/r.git" + ) + assert ( + with_credentials("https://github.com/o/r.git", "git", "t/k@n") + == "https://git:t%2Fk%40n@github.com/o/r.git" + ) + + +def test_with_credentials_keeps_alphanumeric_token_unchanged(): + assert ( + with_credentials("https://github.com/o/r.git", "user", "ghp_AbC123") + == "https://user:ghp_AbC123@github.com/o/r.git" + ) + + +def test_with_credentials_without_credentials_passes_url_through(): + assert with_credentials("https://github.com/o/r.git", None, None) == ( + "https://github.com/o/r.git" + ) + + +def test_with_credentials_requires_both_parts(): + with pytest.raises(InvalidArgumentException): + with_credentials("https://github.com/o/r.git", "user", None) + with pytest.raises(InvalidArgumentException): + with_credentials("https://github.com/o/r.git", None, "token") + + +def test_with_credentials_rejects_non_http_urls(): + with pytest.raises(InvalidArgumentException): + with_credentials("ssh://git@github.com/o/r.git", "user", "token")