Skip to content

Commit c25d0a1

Browse files
veeceeyclaude
andauthored
Fix expand_db_url to handle special characters in passwords (tortoise#2081)
* Fix expand_db_url to handle special characters in passwords urlparse.urlparse() raises ValueError when passwords contain special characters like '[' or ']' because it tries to interpret them as IPv6 address brackets. This fix adds a helper function that percent-encodes the userinfo part (username:password) before parsing, while preserving already-encoded sequences and leaving the rest of the URL intact. Fixes tortoise#404 * Fix code style to pass make check Apply ruff format (double quotes, slice spacing) and remove unused pytest import to satisfy the CI linting pipeline. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 497724c commit c25d0a1

File tree

2 files changed

+80
-0
lines changed

2 files changed

+80
-0
lines changed

tests/test_issue_404.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""Test for issue #404 - expand_db_url fails with special characters in password."""
2+
3+
from tortoise.backends.base.config_generator import expand_db_url
4+
5+
6+
def test_expand_db_url_with_brackets_in_password():
7+
"""Test that passwords with square brackets work correctly."""
8+
# Password with square brackets (as mentioned in issue #404)
9+
db_url = "mysql://some_user:ADM[r$VIS]@test-rds.somedata.net:3306/mydb?charset=utf8mb4"
10+
11+
config = expand_db_url(db_url)
12+
13+
assert config["engine"] == "tortoise.backends.mysql"
14+
assert config["credentials"]["user"] == "some_user"
15+
assert config["credentials"]["password"] == "ADM[r$VIS]"
16+
assert config["credentials"]["host"] == "test-rds.somedata.net"
17+
assert config["credentials"]["port"] == 3306
18+
assert config["credentials"]["database"] == "mydb"
19+
assert config["credentials"]["charset"] == "utf8mb4"
20+
21+
22+
def test_expand_db_url_with_unbalanced_brackets_in_password():
23+
"""Test that passwords with unbalanced square brackets work correctly."""
24+
# Password with unbalanced bracket (causes IPv6 parsing error)
25+
db_url = "mysql://fail_user:DMK_15[ZWIN6@test-rds.somedata.net:3306/mydb2?charset=utf8mb4"
26+
27+
config = expand_db_url(db_url)
28+
29+
assert config["engine"] == "tortoise.backends.mysql"
30+
assert config["credentials"]["user"] == "fail_user"
31+
assert config["credentials"]["password"] == "DMK_15[ZWIN6"
32+
assert config["credentials"]["host"] == "test-rds.somedata.net"
33+
assert config["credentials"]["port"] == 3306
34+
assert config["credentials"]["database"] == "mydb2"

tortoise/backends/base/config_generator.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,53 @@
129129
DB_LOOKUP["postgres"] = DB_LOOKUP["asyncpg"]
130130

131131

132+
def _quote_url_userinfo(db_url: str) -> str:
133+
"""Quote special characters in username and password to avoid URL parsing issues.
134+
135+
urlparse fails when passwords contain characters like '[' or ']' because it
136+
tries to parse them as IPv6 addresses. This function percent-encodes the userinfo
137+
part (username:password) while leaving the rest of the URL intact.
138+
139+
Only characters that cause parsing issues (like '[' and ']') are encoded.
140+
Already percent-encoded sequences (like %25) are preserved.
141+
"""
142+
# Find the scheme delimiter
143+
scheme_end = db_url.find("://")
144+
if scheme_end == -1:
145+
return db_url
146+
147+
scheme = db_url[: scheme_end + 3] # Include "://"
148+
rest = db_url[scheme_end + 3 :]
149+
150+
# Find the userinfo section (everything before @)
151+
at_pos = rest.find("@")
152+
if at_pos == -1:
153+
# No credentials
154+
return db_url
155+
156+
userinfo = rest[:at_pos]
157+
after_userinfo = rest[at_pos:]
158+
159+
# Split userinfo into username and password
160+
colon_pos = userinfo.find(":")
161+
if colon_pos == -1:
162+
# No password, just username
163+
# Only quote characters that cause parsing issues
164+
username = urlparse.quote(userinfo, safe="%")
165+
return scheme + username + after_userinfo
166+
else:
167+
username = userinfo[:colon_pos]
168+
password = userinfo[colon_pos + 1 :]
169+
# Quote username and password, but preserve already-encoded sequences
170+
# We keep % as safe so existing percent-encoded chars aren't double-encoded
171+
username_quoted = urlparse.quote(username, safe="%")
172+
password_quoted = urlparse.quote(password, safe="%")
173+
return scheme + username_quoted + ":" + password_quoted + after_userinfo
174+
175+
132176
def expand_db_url(db_url: str, testing: bool = False) -> dict:
177+
# Quote special characters in userinfo to avoid parsing errors
178+
db_url = _quote_url_userinfo(db_url)
133179
url = urlparse.urlparse(db_url)
134180
if url.scheme not in DB_LOOKUP:
135181
raise ConfigurationError(f"Unknown DB scheme: {url.scheme}")

0 commit comments

Comments
 (0)