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
12 changes: 12 additions & 0 deletions .changeset/python-git-credentials-encode.md
Original file line number Diff line number Diff line change
@@ -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.
16 changes: 16 additions & 0 deletions packages/js-sdk/tests/sandbox/git/validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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')
})
4 changes: 2 additions & 2 deletions packages/python-sdk/e2b/sandbox/_git/auth.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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))


Expand Down
46 changes: 46 additions & 0 deletions packages/python-sdk/tests/shared/git/test_auth.py
Original file line number Diff line number Diff line change
@@ -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")