proxy: decode username and password (#6700)

## Problem

usernames and passwords can be URL 'percent' encoded in the connection
string URL provided by serverless driver.

## Summary of changes

Decode the parameters when getting conn info
This commit is contained in:
Conrad Ludgate
2024-02-09 19:22:23 +00:00
committed by GitHub
parent ca818c8bd7
commit cbd3a32d4d
8 changed files with 34 additions and 10 deletions

2
Cargo.lock generated
View File

@@ -4125,6 +4125,7 @@ dependencies = [
"serde",
"serde_json",
"sha2",
"smallvec",
"smol_str",
"socket2 0.5.5",
"sync_wrapper",
@@ -4143,6 +4144,7 @@ dependencies = [
"tracing-subscriber",
"tracing-utils",
"url",
"urlencoding",
"utils",
"uuid",
"walkdir",

View File

@@ -171,6 +171,7 @@ tracing-opentelemetry = "0.20.0"
tracing-subscriber = { version = "0.3", default_features = false, features = ["smallvec", "fmt", "tracing-log", "std", "env-filter", "json"] }
twox-hash = { version = "1.6.3", default-features = false }
url = "2.2"
urlencoding = "2.1"
uuid = { version = "1.6.1", features = ["v4", "v7", "serde"] }
walkdir = "2.3.2"
webpki-roots = "0.25"

View File

@@ -60,6 +60,8 @@ scopeguard.workspace = true
serde.workspace = true
serde_json.workspace = true
sha2.workspace = true
smol_str.workspace = true
smallvec.workspace = true
socket2.workspace = true
sync_wrapper.workspace = true
task-local-extensions.workspace = true
@@ -76,6 +78,7 @@ tracing-subscriber.workspace = true
tracing-utils.workspace = true
tracing.workspace = true
url.workspace = true
urlencoding.workspace = true
utils.workspace = true
uuid.workspace = true
webpki-roots.workspace = true
@@ -84,7 +87,6 @@ native-tls.workspace = true
postgres-native-tls.workspace = true
postgres-protocol.workspace = true
redis.workspace = true
smol_str.workspace = true
workspace_hack.workspace = true

View File

@@ -48,7 +48,7 @@ impl PoolingBackend {
}
};
let auth_outcome =
crate::auth::validate_password_and_exchange(conn_info.password.as_bytes(), secret)?;
crate::auth::validate_password_and_exchange(&conn_info.password, secret)?;
match auth_outcome {
crate::sasl::Outcome::Success(key) => Ok(key),
crate::sasl::Outcome::Failure(reason) => {

View File

@@ -3,6 +3,7 @@ use futures::{future::poll_fn, Future};
use metrics::IntCounterPairGuard;
use parking_lot::RwLock;
use rand::Rng;
use smallvec::SmallVec;
use smol_str::SmolStr;
use std::{collections::HashMap, pin::pin, sync::Arc, sync::Weak, time::Duration};
use std::{
@@ -36,7 +37,7 @@ pub const APP_NAME: SmolStr = SmolStr::new_inline("/sql_over_http");
pub struct ConnInfo {
pub user_info: ComputeUserInfo,
pub dbname: DbName,
pub password: SmolStr,
pub password: SmallVec<[u8; 16]>,
}
impl ConnInfo {
@@ -731,7 +732,7 @@ mod tests {
options: Default::default(),
},
dbname: "dbname".into(),
password: "password".into(),
password: "password".as_bytes().into(),
};
let ep_pool =
Arc::downgrade(&pool.get_or_create_endpoint_pool(&conn_info.endpoint_cache_key()));
@@ -788,7 +789,7 @@ mod tests {
options: Default::default(),
},
dbname: "dbname".into(),
password: "password".into(),
password: "password".as_bytes().into(),
};
let ep_pool =
Arc::downgrade(&pool.get_or_create_endpoint_pool(&conn_info.endpoint_cache_key()));

View File

@@ -100,6 +100,8 @@ pub enum ConnInfoError {
InvalidDbName,
#[error("missing username")]
MissingUsername,
#[error("invalid username: {0}")]
InvalidUsername(#[from] std::string::FromUtf8Error),
#[error("missing password")]
MissingPassword,
#[error("missing hostname")]
@@ -134,7 +136,7 @@ fn get_conn_info(
let dbname = url_path.next().ok_or(ConnInfoError::InvalidDbName)?;
let username = RoleName::from(connection_url.username());
let username = RoleName::from(urlencoding::decode(connection_url.username())?);
if username.is_empty() {
return Err(ConnInfoError::MissingUsername);
}
@@ -143,6 +145,7 @@ fn get_conn_info(
let password = connection_url
.password()
.ok_or(ConnInfoError::MissingPassword)?;
let password = urlencoding::decode_binary(password.as_bytes());
let hostname = connection_url
.host_str()
@@ -172,7 +175,10 @@ fn get_conn_info(
Ok(ConnInfo {
user_info,
dbname: dbname.into(),
password: password.into(),
password: match password {
std::borrow::Cow::Borrowed(b) => b.into(),
std::borrow::Cow::Owned(b) => b.into(),
},
})
}

View File

@@ -23,7 +23,7 @@ from itertools import chain, product
from pathlib import Path
from types import TracebackType
from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Type, Union, cast
from urllib.parse import urlparse
from urllib.parse import quote, urlparse
import asyncpg
import backoff
@@ -2822,8 +2822,8 @@ class NeonProxy(PgProtocol):
def http_query(self, query, args, **kwargs):
# TODO maybe use default values if not provided
user = kwargs["user"]
password = kwargs["password"]
user = quote(kwargs["user"])
password = quote(kwargs["password"])
expected_code = kwargs.get("expected_code")
connstr = f"postgresql://{user}:{password}@{self.domain}:{self.proxy_port}/postgres"

View File

@@ -462,6 +462,18 @@ def test_sql_over_http_pool(static_proxy: NeonProxy):
assert "password authentication failed for user" in res["message"]
def test_sql_over_http_urlencoding(static_proxy: NeonProxy):
static_proxy.safe_psql("create user \"http+auth$$\" with password '%+$^&*@!' superuser")
static_proxy.http_query(
"select 1",
[],
user="http+auth$$",
password="%+$^&*@!",
expected_code=200,
)
# Beginning a transaction should not impact the next query,
# which might come from a completely different client.
def test_http_pool_begin(static_proxy: NeonProxy):