Skip to content

unquote_plus double-decodes URL credentials, corrupting passwords containing + #611

Description

@liprais

Expected behavior

Expected behavior

Passwords containing URL-safe special characters (like +, %, @) should survive the URL → connect round-trip unchanged:

trino://user:pass+word@host/catalog → BasicAuthentication('user', 'pass+word')

The password should not be modified beyond SQLAlchemy's own URL decoding.

Actual behavior

Version

trino 0.337.0

Bug location

trino/sqlalchemy/dialect.pyTrinoDialect.create_connect_args

Root cause

url.password and url.username are already decoded by SQLAlchemy's make_url(). The dialect then applies unquote_plus again:

trino://user:pass%2Bword@host/catalog

→ SQLAlchemy make_url decodes %2B+
url.password = 'pass+word'

create_connect_args calls unquote_plus('pass+word')'pass word'

Impact

Any password containing a literal + character gets corrupted to a space, causing "Access Denied: Invalid credentials" (401).

Precedent

SQLAlchemy itself fixed the exact same issue 11 years ago:
sqlalchemy/sqlalchemy#2873
Their fix: use unquote for passwords, not unquote_plus.

Fix

Replace unquote_plus(url.username) and unquote_plus(url.password) with url.username / url.password directly — they're already decoded.

(Alternatively, use urllib.parse.unquote instead of unquote_plus for these fields.)

Workaround

Monkey-patch before creating the engine:

import trino.sqlalchemy.dialect
from urllib.parse import unquote
trino.sqlalchemy.dialect.unquote_plus = unquote


### Steps To Reproduce

```python
from sqlalchemy import create_engine
from trino.auth import BasicAuthentication

# Password contains a literal '+' character
url = "trino://user:pass+word@localhost:8080/catalog"

engine = create_engine(url)

# The dialect creates BasicAuthentication with corrupted password:
#   expected: BasicAuthentication('user', 'pass+word')
#   actual:   BasicAuthentication('user', 'pass word')
#                       the '+' became a space ↑

Or test the dialect directly:

from sqlalchemy.engine.url import make_url
from trino.sqlalchemy.dialect import TrinoDialect

url = make_url("trino://user:pass+word@localhost:8080/catalog")
dialect = TrinoDialect()
_, kwargs = dialect.create_connect_args(url)

auth = kwargs["auth"]
print(auth._username)  # 'user'  
print(auth._password)  # 'pass word'  ← should be 'pass+word'

Result: connection fails with error 401: Access Denied: Invalid credentials.

Log output

No response

Operating System

Debian GNU/Linux 12 (bookworm)

Trino Python client version

0.337.0

Trino Server version

480

Python version

python3.10

Are you willing to submit PR?

  • Yes I am willing to submit a PR!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions