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
16 changes: 0 additions & 16 deletions .github/workflows/pr-code-security.yml

This file was deleted.

31 changes: 31 additions & 0 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Security audit

on:
push:
branches: [main]
pull_request:
schedule:
# Re-run weekly so newly-published advisories are caught even without a push.
- cron: "0 6 * * 1"

permissions:
contents: read

jobs:
cargo-deny:
name: cargo-deny (advisories, bans, sources)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: stable
# Run cargo-deny directly on the runner (not the musl container action,
# which trips over the repo's `rust-toolchain` file) and fetch a fresh
# advisory DB each run.
- name: Install cargo-deny
uses: taiki-e/install-action@cargo-deny
- name: Check advisories, bans, sources
run: cargo deny check advisories bans sources
19 changes: 19 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,25 @@ jobs:
- name: Install dependencies
run: sudo apt install -y openssl libkrb5-dev

- name: Wait for SQL Server
# A listening port is not readiness: SQL Server binds 1433 before the SA
# login and databases finish initializing, so tests started too early race
# it and hit sporadic connection/login failures. Gate on an authenticated
# `SELECT 1` from a throwaway mssql-tools container (works uniformly across
# the full server images and azure-sql-edge, which ships no in-box sqlcmd).
run: |
pw='<YourStrong@Passw0rd>'
for _ in $(seq 1 60); do
if docker run --rm --network host mcr.microsoft.com/mssql-tools \
/opt/mssql-tools/bin/sqlcmd -S localhost,1433 -U SA -P "$pw" -Q "SELECT 1" >/dev/null 2>&1; then
echo "SQL Server ready (authenticated login succeeded)"; exit 0
fi
sleep 3
done
echo "SQL Server did not accept an authenticated login in time" >&2
docker compose -f docker-compose.yml logs mssql-${{matrix.database}} || true
exit 1

- name: Run tests
run: cargo test ${{matrix.features}}

Expand Down
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -168,13 +168,13 @@ path = "./runtimes-macro"
[dev-dependencies]
names = "0.14"
anyhow = "1"
env_logger = "0.9"
env_logger = "0.11"
azure_identity = "0.20.0"
oauth2 = "5.0"
url = "2.2.2"
reqwest = "0.12"
paste = "1.0"
indicatif = "0.17"
indicatif = "0.18"
chrono = "0.4.38"
indoc = "1.0.7"

Expand Down
38 changes: 38 additions & 0 deletions deny.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# cargo-deny configuration — supply-chain / advisory gate for tiberius.
#
# Run locally with: cargo deny check advisories bans sources
# CI runs the same via .github/workflows/security.yml.
#
# Policy: any security *vulnerability* or *yanked* crate in the actually-built
# dependency graph fails the build. The `ignore` list below contains only
# advisories that are provably NOT part of the shipped library — they come from
# dev-dependencies (tests/examples) or from opt-in, non-default features — so
# they cannot affect a downstream user of the default crate. Each entry is
# justified; revisit whenever the upstream tooling gains a maintained successor.

[advisories]
yanked = "deny"
ignore = [
# async-std is discontinued upstream. In tiberius it is reachable ONLY through
# the opt-in `sql-browser-async-std` feature (and dev-deps); it is not part of
# the default build. Tracked for migration to smol/tokio.
{ id = "RUSTSEC-2025-0052", reason = "async-std: opt-in `sql-browser-async-std` feature + dev-deps only; not in the default shipped graph" },

# The following are ALL dev-dependency-only (test harness + the aad-auth
# example) and are never compiled into the published library.
{ id = "RUSTSEC-2024-0375", reason = "atty: dev-dependency only (via `names` -> clap 3); not shipped" },
{ id = "RUSTSEC-2024-0370", reason = "proc-macro-error: dev-dependency only (via `names` -> clap 3); not shipped" },
{ id = "RUSTSEC-2024-0384", reason = "instant: transitive dev-dependency only; not shipped" },
{ id = "RUSTSEC-2024-0436", reason = "paste: dev/test only (tests/bulk.rs + azure_identity example); not shipped" },
{ id = "RUSTSEC-2026-0174", reason = "http-types: dev-only via azure_identity in the aad-auth example; not shipped" },
{ id = "RUSTSEC-2026-0275", reason = "azure_core: dev-only via azure_identity in the aad-auth example; not shipped (the crate never handles AAD tokens itself)" },
]

[bans]
multiple-versions = "warn"
wildcards = "allow"

[sources]
unknown-registry = "deny"
unknown-git = "deny"
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
2 changes: 1 addition & 1 deletion src/client/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ pub(crate) trait ConfigString {
fn readonly(&self) -> bool {
self.dict()
.get("applicationintent")
.filter(|val| *val == "ReadOnly")
.filter(|val| val.trim().eq_ignore_ascii_case("ReadOnly"))
.is_some()
}
}
21 changes: 21 additions & 0 deletions src/client/config/ado_net.rs
Original file line number Diff line number Diff line change
Expand Up @@ -484,4 +484,25 @@ mod tests {

Ok(())
}

#[test]
fn application_intent_readonly_parsing() -> crate::Result<()> {
// Exact spelling from the ADO.NET connection string.
let ado: AdoNetConfig = "ApplicationIntent=ReadOnly".parse()?;
assert!(ado.readonly());

// ADO.NET treats the value case-insensitively.
let ado: AdoNetConfig = "applicationintent=readonly".parse()?;
assert!(ado.readonly());

// ReadWrite (the default) must not request read-only intent.
let ado: AdoNetConfig = "ApplicationIntent=ReadWrite".parse()?;
assert!(!ado.readonly());

// Absent altogether.
let ado: AdoNetConfig = "server=tcp:localhost,1433".parse()?;
assert!(!ado.readonly());

Ok(())
}
}
60 changes: 55 additions & 5 deletions src/client/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ impl<S: AsyncRead + AsyncWrite + Unpin + Send> Connection<S> {
.prelogin(config.encryption, fed_auth_required)
.await?;

let encryption = prelogin.negotiated_encryption(config.encryption);
let encryption = prelogin.negotiated_encryption(config.encryption)?;

let connection = connection.tls_handshake(&config, encryption).await?;

Expand Down Expand Up @@ -285,7 +285,7 @@ impl<S: AsyncRead + AsyncWrite + Unpin + Send> Connection<S> {
/// Defines the login record rules with SQL Server. Authentication with
/// connection options.
#[allow(clippy::too_many_arguments)]
async fn login<'a>(
async fn login(
mut self,
auth: AuthMethod,
encryption: EncryptionLevel,
Expand Down Expand Up @@ -445,7 +445,7 @@ impl<S: AsyncRead + AsyncWrite + Unpin + Send> Connection<S> {
encryption: EncryptionLevel,
) -> crate::Result<Self> {
if encryption != EncryptionLevel::NotSupported {
event!(Level::INFO, "Performing a TLS handshake");
event!(Level::DEBUG, "Performing a TLS handshake");

let Self {
transport, context, ..
Expand All @@ -458,7 +458,7 @@ impl<S: AsyncRead + AsyncWrite + Unpin + Send> Connection<S> {
};

stream.get_mut().handshake_complete();
event!(Level::INFO, "TLS handshake successful");
event!(Level::DEBUG, "TLS handshake successful");

let transport = Framed::new(MaybeTlsStream::Tls(stream), PacketCodec);

Expand All @@ -484,7 +484,12 @@ impl<S: AsyncRead + AsyncWrite + Unpin + Send> Connection<S> {
feature = "native-tls",
feature = "vendored-openssl"
)))]
async fn tls_handshake(self, _: &Config, _: EncryptionLevel) -> crate::Result<Self> {
async fn tls_handshake(self, config: &Config, _: EncryptionLevel) -> crate::Result<Self> {
// Without a TLS backend compiled in, we cannot encrypt anything. If the
// user asked for encryption, fail loudly instead of silently sending
// traffic (including login credentials) in the clear.
check_tls_backend_available(config.encryption)?;

event!(
Level::WARN,
"TLS encryption is not enabled. All traffic including the login credentials are not encrypted."
Expand All @@ -498,6 +503,51 @@ impl<S: AsyncRead + AsyncWrite + Unpin + Send> Connection<S> {
}
}

/// Returns an error when the user requested encryption but no TLS backend was
/// compiled in. Without this check, a `Required`/`On` encryption request would
/// silently fall back to an unencrypted connection.
#[cfg(not(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
)))]
fn check_tls_backend_available(encryption: EncryptionLevel) -> crate::Result<()> {
if let EncryptionLevel::On | EncryptionLevel::Required = encryption {
return Err(crate::Error::Tls(
"TLS encryption was requested but the crate was compiled without a TLS backend. \
Enable one of the `native-tls`, `rustls` or `vendored-openssl` features."
.to_string(),
));
}

Ok(())
}

#[cfg(all(
test,
not(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
))
))]
mod tests {
use super::check_tls_backend_available;
use crate::EncryptionLevel;

#[test]
fn requested_encryption_without_tls_backend_errors() {
assert!(check_tls_backend_available(EncryptionLevel::Required).is_err());
assert!(check_tls_backend_available(EncryptionLevel::On).is_err());
}

#[test]
fn no_encryption_without_tls_backend_is_ok() {
assert!(check_tls_backend_available(EncryptionLevel::Off).is_ok());
assert!(check_tls_backend_available(EncryptionLevel::NotSupported).is_ok());
}
}

impl<S: AsyncRead + AsyncWrite + Unpin + Send> Stream for Connection<S> {
type Item = crate::Result<Packet>;

Expand Down
2 changes: 1 addition & 1 deletion src/client/tls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ impl<S: AsyncRead + AsyncWrite + Unpin + Send> AsyncRead for TlsPreloginWrapper<
}

let header = PacketHeader::decode(&mut BytesMut::from(&inner.header_buf[..]))
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
.map_err(io::Error::other)?;

// We only get pre-login packets in the handshake process.
assert_eq!(header.r#type(), PacketType::PreLogin);
Expand Down
8 changes: 4 additions & 4 deletions src/client/tls_stream/native_tls_stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ pub(crate) async fn create_tls_stream<S: AsyncRead + AsyncWrite + Unpin + Send>(
if let Ok(buf) = fs::read(path) {
let cert = match path.extension() {
Some(ext)
if ext.to_ascii_lowercase() == "pem"
|| ext.to_ascii_lowercase() == "crt" =>
if ext.eq_ignore_ascii_case("pem")
|| ext.eq_ignore_ascii_case("crt") =>
{
Some(Certificate::from_pem(&buf)?)
}
Some(ext) if ext.to_ascii_lowercase() == "der" => {
Some(ext) if ext.eq_ignore_ascii_case("der") => {
Some(Certificate::from_der(&buf)?)
}
Some(_) | None => return Err(Error::Io {
Expand Down Expand Up @@ -52,7 +52,7 @@ pub(crate) async fn create_tls_stream<S: AsyncRead + AsyncWrite + Unpin + Send>(
builder = builder.use_sni(false);
}
TrustConfig::Default => {
event!(Level::INFO, "Using default trust configuration.");
event!(Level::DEBUG, "Using default trust configuration.");
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/client/tls_stream/opentls_tls_stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ pub(crate) async fn create_tls_stream<S: AsyncRead + AsyncWrite + Unpin + Send>(
builder = builder.use_sni(false);
}
TrustConfig::Default => {
event!(Level::INFO, "Using default trust configuration.");
event!(Level::DEBUG, "Using default trust configuration.");
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/client/tls_stream/rustls_tls_stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ fn get_server_name(config: &Config) -> crate::Result<ServerName<'static>> {

impl<S: AsyncRead + AsyncWrite + Unpin + Send> TlsStream<S> {
pub(super) async fn new(config: &Config, stream: S) -> crate::Result<Self> {
event!(Level::INFO, "Performing a TLS handshake");
event!(Level::DEBUG, "Performing a TLS handshake");

// Negotiate the best available protocol version (TLS 1.2 or 1.3), the
// same policy as upstream's previous `with_safe_defaults()`.
Expand Down Expand Up @@ -171,7 +171,7 @@ impl<S: AsyncRead + AsyncWrite + Unpin + Send> TlsStream<S> {
.with_no_client_auth()
}
TrustConfig::Default => {
event!(Level::INFO, "Using default trust configuration.");
event!(Level::DEBUG, "Using default trust configuration.");
builder.with_native_roots().with_no_client_auth()
}
};
Expand Down
21 changes: 20 additions & 1 deletion src/from_sql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ where
from_sql!(bool: ColumnData::Bit(val) => (*val, val));
from_sql!(u8: ColumnData::U8(val) => (*val, val), ColumnData::I32(None) => (None, None));
from_sql!(i16: ColumnData::I16(val) => (*val, val), ColumnData::U8(None) => (None, None), ColumnData::I32(None) => (None, None));
from_sql!(i32: ColumnData::I32(val) => (*val, val), ColumnData::U8(None) => (None, None));
from_sql!(i32: ColumnData::I32(val) => (*val, val), ColumnData::I16(val) => (val.map(i32::from), val.map(i32::from)), ColumnData::U8(None) => (None, None));
from_sql!(i64: ColumnData::I64(val) => (*val, val), ColumnData::U8(None) => (None, None), ColumnData::I32(None) => (None, None));
from_sql!(f32: ColumnData::F32(val) => (*val, val));
from_sql!(f64: ColumnData::F64(val) => (*val, val));
Expand Down Expand Up @@ -132,3 +132,22 @@ impl<'a> FromSql<'a> for &'a [u8] {
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn i16_column_converts_to_i32() {
let data = ColumnData::I16(Some(8));
assert_eq!(Some(8i32), i32::from_sql(&data).unwrap());
assert_eq!(Some(8i32), i32::from_sql_owned(data).unwrap());
}

#[test]
fn null_i16_column_converts_to_i32() {
let data = ColumnData::I16(None);
assert_eq!(None, i32::from_sql(&data).unwrap());
assert_eq!(None, i32::from_sql_owned(ColumnData::I16(None)).unwrap());
}
}
6 changes: 3 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,11 +156,11 @@
//! Tiberius supports different [ways of authentication] to the SQL Server:
//!
//! - SQL Server authentication uses the facilities of the database to
//! authenticate the user.
//! authenticate the user.
//! - On Windows, you can authenticate using the currently logged in user or
//! specified Windows credentials.
//! specified Windows credentials.
//! - If enabling the `integrated-auth-gssapi` feature, it is possible to login
//! with the currently active Kerberos credentials.
//! with the currently active Kerberos credentials.
//!
//! ## AAD(Azure Active Directory) Authentication
//!
Expand Down
5 changes: 4 additions & 1 deletion src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ macro_rules! uint_enum {
type Error = ();
fn try_from(n: u8) -> ::std::result::Result<$ty, ()> {
match n {
$( x if x == $ty::$variant as u8 => Ok($ty::$variant), )*
// Generic macro codegen: the `as u8` cast is compared against a `u8`
// input, so wider enum variants can never match here (they fall through
// to `Err`). The truncation is intentional and harmless.
$( #[allow(clippy::cast_enum_truncation)] x if x == $ty::$variant as u8 => Ok($ty::$variant), )*
_ => Err(()),
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ impl<'a> Query<'a> {
/// [`ToSql`]: trait.ToSql.html
/// [`FromSql`]: trait.FromSql.html
/// [`Client#execute`]: struct.Client.html#method.execute
pub async fn execute<'b, S>(self, client: &'b mut Client<S>) -> crate::Result<ExecuteResult>
pub async fn execute<S>(self, client: &mut Client<S>) -> crate::Result<ExecuteResult>
where
S: AsyncRead + AsyncWrite + Unpin + Send,
{
Expand Down
Loading
Loading