fix(deps): upgrade crates with security advisories (#338)

cargo deny check now passes, with a deny.toml at the repository root (advisories, licenses, bans, sources).

- zip 2.4.2 → 9 as one workspace dependency (RUSTSEC-2026-0367/0368/0369). Our own extraction is hardened too: tar/zip entries cannot escape the destination, symlinks are allowed only when they resolve inside it, entries cannot traverse a symlink, and setuid/setgid/sticky bits are dropped (also after system tar).
- tiberius 0.12 → 0.13 with rustls 0.23, which brings the fixed rustls / rustls-webpki (RUSTSEC-2026-0098/0099/0104/0346/0349/0382/0383). The bb8-tiberius adapter is replaced by a small bb8 connection manager in ide-db that keeps its single-redirect handling.
- crypto-bigint / ctutils updated (RUSTSEC-2026-0385/0387).
- rsa RUSTSEC-2023-0071 (no fixed release) is ignored with its reason: RSA is used only for MySQL password encryption and SSH signatures, never to decrypt attacker-controlled ciphertext.
- Unmaintained transitive crates (paste, proc-macro-error, rustls-pemfile, zbus/zvariant) remain warnings. MPL-2.0 is allowed (file-level copyleft, dependencies used unmodified).

未在真实 SQL Server 及 Windows / Linux 上验证解压。
This commit is contained in:
l0ng-ai authored and GitHub committed 2026-10-30 11:36:51 +08:00
1 parent b1be3c6a09
commit 7da58637c2
15 files changed
+565 -205

No files matched your search

Generated
+132 -176
View File
@@ -128,9 +128,6 @@ name = "arbitrary"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bc62ac97cc33321f50863d514c3bc38a453947a8f9e781137e47c7401020aed"
dependencies = [
"derive_arbitrary",
]
[[package]]
name = "arc-swap"
@@ -314,19 +311,6 @@ dependencies = [
"syn 3.0.9",
]
[[package]]
name = "asynchronous-codec"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4057f2c32adbb2fc158e22fb38433c8e9bbf76b75a4732c7c0cbaf695fb65568"
dependencies = [
"bytes",
"futures-sink",
"futures-util",
"memchr",
"pin-project-lite",
]
[[package]]
name = "asynchronous-codec"
version = "0.7.0"
@@ -384,6 +368,29 @@ version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "aws-lc-rs"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "048006790404103c488e6c5616b3e7e702a71292aadda08e8f7698a9e0d2eb53"
dependencies = [
"aws-lc-sys",
"zeroize",
]
[[package]]
name = "aws-lc-sys"
version = "0.47.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bfbe32ac36b3c428696ce315464181bc4029d689fd58e3f2a4e58c252242f52"
dependencies = [
"cc",
"cmake",
"dunce",
"fs_extra",
"pkg-config",
]
[[package]]
name = "base16ct"
version = "1.0.0"
@@ -426,19 +433,6 @@ dependencies = [
"tokio",
]
[[package]]
name = "bb8-tiberius"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a5023b318f47a294d02b46ce0d82ac8e7df0afce2257f041ba7316566d6cd56"
dependencies = [
"bb8",
"thiserror 1.0.69",
"tiberius",
"tokio",
"tokio-util",
]
[[package]]
name = "bcrypt-pbkdf"
version = "0.11.0"
@@ -799,6 +793,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877bf67ac8e52449eb2f9e98a344ea163beb70dd1f9eebe3cc5357dcfeaee4df"
dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex",
]
@@ -958,6 +954,15 @@ dependencies = [
"hashbrown 0.16.1",
]
[[package]]
name = "cmake"
version = "0.1.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
dependencies = [
"cc",
]
[[package]]
name = "cmov"
version = "0.5.4"
@@ -1393,9 +1398,9 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "crypto-bigint"
version = "0.7.5"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271"
checksum = "9c035cb64cba03dad2fb921f3e218785e8cf9a41790da5eb3a1400f62fa2be11"
dependencies = [
"cpubits",
"ctutils",
@@ -1479,9 +1484,9 @@ dependencies = [
[[package]]
name = "ctutils"
version = "0.4.3"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03bb0e1cc970d482d121d9a1744999169b69a07470b3d644a7894e53fcaf4574"
checksum = "a2376ffd6f96d8a9db59875b4b6b0237d6900b5baf64279714e05493ce64828a"
dependencies = [
"cmov",
"subtle",
@@ -1685,17 +1690,6 @@ dependencies = [
"serde_core",
]
[[package]]
name = "derive_arbitrary"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b034bd7d5f032402a2479444dcc6f74e36a03f31854d41680fb240ef682a1ac"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
]
[[package]]
name = "derive_more"
version = "2.1.1"
@@ -2295,6 +2289,12 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "fs_extra"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "fsevent-sys"
version = "4.1.0"
@@ -2399,6 +2399,12 @@ version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
[[package]]
name = "futures-timer"
version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968"
[[package]]
name = "futures-util"
version = "0.3.34"
@@ -3874,9 +3880,9 @@ dependencies = [
"http",
"hyper",
"hyper-util",
"rustls 0.23.46",
"rustls",
"tokio",
"tokio-rustls 0.26.6",
"tokio-rustls",
"tower-service",
]
@@ -4142,7 +4148,6 @@ name = "ide-db"
version = "0.1.26"
dependencies = [
"bb8",
"bb8-tiberius",
"dirs 6.0.0",
"futures",
"ide-core",
@@ -4155,6 +4160,7 @@ dependencies = [
"tempfile",
"tiberius",
"tokio",
"tokio-util",
"toml 0.9.12+spec-1.1.0",
"tracing",
]
@@ -4250,7 +4256,7 @@ dependencies = [
"ide-core",
"parking_lot",
"reqwest",
"rustls 0.23.46",
"rustls",
"rustls-platform-verifier",
"serde",
"serde_json",
@@ -4465,7 +4471,7 @@ dependencies = [
"toml 0.9.12+spec-1.1.0",
"toml_edit 0.25.17+spec-1.1.0",
"tracing",
"zip 2.4.2",
"zip",
]
[[package]]
@@ -4485,7 +4491,7 @@ dependencies = [
"toml 0.9.12+spec-1.1.0",
"tracing",
"which",
"zip 2.4.2",
"zip",
]
[[package]]
@@ -4598,7 +4604,7 @@ dependencies = [
"tempfile",
"tracing",
"which",
"zip 2.4.2",
"zip",
]
[[package]]
@@ -4639,7 +4645,7 @@ dependencies = [
"tempfile",
"tracing",
"trash",
"zip 2.4.2",
"zip",
]
[[package]]
@@ -4677,7 +4683,7 @@ dependencies = [
"toml 0.9.12+spec-1.1.0",
"toml_edit 0.25.17+spec-1.1.0",
"tracing",
"zip 2.4.2",
"zip",
]
[[package]]
@@ -5046,6 +5052,16 @@ dependencies = [
"syn 2.0.119",
]
[[package]]
name = "jobserver"
version = "0.1.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3"
dependencies = [
"getrandom 0.4.3",
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.106"
@@ -6179,12 +6195,6 @@ dependencies = [
"tracing-subscriber",
]
[[package]]
name = "openssl-probe"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "openssl-probe"
version = "0.2.1"
@@ -6663,9 +6673,9 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "pretty-hex"
version = "0.3.0"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5"
checksum = "9a65843dfefbafd3c879c683306959a6de478443ffe9c9adf02f5976432402d7"
[[package]]
name = "primefield"
@@ -7061,14 +7071,14 @@ dependencies = [
"mime",
"percent-encoding",
"pin-project-lite",
"rustls 0.23.46",
"rustls",
"rustls-pki-types",
"rustls-platform-verifier",
"serde",
"serde_json",
"sync_wrapper",
"tokio",
"tokio-rustls 0.26.6",
"tokio-rustls",
"tokio-util",
"tower",
"tower-http",
@@ -7330,64 +7340,32 @@ dependencies = [
"rustix",
]
[[package]]
name = "rustls"
version = "0.21.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
dependencies = [
"log",
"ring",
"rustls-webpki 0.101.7",
"sct",
]
[[package]]
name = "rustls"
version = "0.23.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf"
dependencies = [
"aws-lc-rs",
"log",
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki 0.103.16",
"rustls-webpki",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-native-certs"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
dependencies = [
"openssl-probe 0.1.6",
"rustls-pemfile",
"schannel",
"security-framework 2.11.1",
]
[[package]]
name = "rustls-native-certs"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
dependencies = [
"openssl-probe 0.2.1",
"openssl-probe",
"rustls-pki-types",
"schannel",
"security-framework 3.7.0",
]
[[package]]
name = "rustls-pemfile"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
dependencies = [
"base64 0.21.7",
"security-framework",
]
[[package]]
@@ -7410,11 +7388,11 @@ dependencies = [
"jni 0.22.4",
"log",
"once_cell",
"rustls 0.23.46",
"rustls-native-certs 0.8.4",
"rustls",
"rustls-native-certs",
"rustls-platform-verifier-android",
"rustls-webpki 0.103.16",
"security-framework 3.7.0",
"rustls-webpki",
"security-framework",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.61.2",
@@ -7426,22 +7404,13 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f"
[[package]]
name = "rustls-webpki"
version = "0.101.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "rustls-webpki"
version = "0.103.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281"
dependencies = [
"aws-lc-rs",
"ring",
"rustls-pki-types",
"untrusted",
@@ -7578,16 +7547,6 @@ dependencies = [
"sha2 0.11.1",
]
[[package]]
name = "sct"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "sdd"
version = "4.8.11"
@@ -7612,16 +7571,12 @@ dependencies = [
]
[[package]]
name = "security-framework"
version = "2.11.1"
name = "secrecy"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a"
dependencies = [
"bitflags 2.13.2",
"core-foundation 0.9.4",
"core-foundation-sys",
"libc",
"security-framework-sys",
"zeroize",
]
[[package]]
@@ -8226,7 +8181,7 @@ dependencies = [
"log",
"memchr",
"percent-encoding",
"rustls 0.23.46",
"rustls",
"serde",
"serde_json",
"sha2 0.10.9",
@@ -8915,11 +8870,11 @@ dependencies = [
[[package]]
name = "tauri-plugin-updater"
version = "2.14.0"
version = "2.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7259989d4fe11b2bf51f7d671642249b1837c4ae2da1430dc713ae38f98ceeec"
checksum = "d957abbcb3f9f6c76d22a3a0f012f30880640d0bccb5634d2b12a9937e4c872b"
dependencies = [
"base64 0.22.1",
"base64 0.23.1",
"dirs 7.0.0",
"flate2",
"futures-util",
@@ -8931,7 +8886,7 @@ dependencies = [
"osakit",
"percent-encoding",
"reqwest",
"rustls 0.23.46",
"rustls",
"semver",
"serde",
"serde_json",
@@ -8944,7 +8899,7 @@ dependencies = [
"tokio",
"url",
"windows-sys 0.61.2",
"zip 4.6.1",
"zip",
]
[[package]]
@@ -9175,30 +9130,47 @@ dependencies = [
[[package]]
name = "tiberius"
version = "0.12.3"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1446cb4198848d1562301a3340424b4f425ef79f35ef9ee034769a9dd92c10d"
checksum = "e07324791de2bdaed058af4aa433a0b5ec5c0beac407a9797419c19805df2415"
dependencies = [
"async-trait",
"asynchronous-codec 0.6.2",
"asynchronous-codec",
"byteorder",
"bytes",
"connection-string",
"encoding_rs",
"enumflags2",
"futures-timer",
"futures-util",
"libc",
"num-traits",
"once_cell",
"pin-project-lite",
"pretty-hex",
"rustls-native-certs 0.6.3",
"rustls-pemfile",
"thiserror 1.0.69",
"rustls-native-certs",
"rustls-pki-types",
"rustls-webpki",
"secrecy",
"thiserror 2.0.21",
"tiberius-macros",
"tokio",
"tokio-rustls 0.24.1",
"tokio-rustls",
"tokio-util",
"tracing",
"uuid",
"zeroize",
]
[[package]]
name = "tiberius-macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "907fed5ea55695d902cc3359fffa4a35021dfa9b95ab4eb9c8376d42ffb9eec8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
@@ -9285,23 +9257,13 @@ dependencies = [
"syn 3.0.9",
]
[[package]]
name = "tokio-rustls"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
dependencies = [
"rustls 0.21.12",
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.26.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db"
dependencies = [
"rustls 0.23.46",
"rustls",
"tokio",
]
@@ -10023,6 +9985,12 @@ dependencies = [
"utf-8",
]
[[package]]
name = "typed-path"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce898823d8cd0f028662d2f349694b1af0a5cc9be83f1d213a00d3147445e1e"
[[package]]
name = "typeid"
version = "1.0.3"
@@ -10689,9 +10657,9 @@ dependencies = [
[[package]]
name = "webpki-root-certs"
version = "1.0.9"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b"
checksum = "e49d0e65abc00c142e8f9428da991da9f1a250c39be97459901eb12b9b49f62a"
dependencies = [
"rustls-pki-types",
]
@@ -11521,7 +11489,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efb2c254fd8f366755335c9e43b865f8484fe3bd717d65ffe7c3f28852863030"
dependencies = [
"async-trait",
"asynchronous-codec 0.7.0",
"asynchronous-codec",
"bytes",
"crossbeam-queue",
"futures",
@@ -11574,33 +11542,21 @@ dependencies = [
[[package]]
name = "zip"
version = "2.4.2"
version = "9.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50"
checksum = "1156b39d142a34b999c2310a50455e0cb3760bf7efad0122f796d813bee26ccb"
dependencies = [
"arbitrary",
"crc32fast",
"crossbeam-utils",
"displaydoc",
"flate2",
"getrandom 0.4.3",
"indexmap 2.14.2",
"memchr",
"thiserror 2.0.21",
"nix 0.31.3",
"typed-path",
"windows-sys 0.61.2",
"zopfli",
]
[[package]]
name = "zip"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
dependencies = [
"arbitrary",
"crc32fast",
"indexmap 2.14.2",
"memchr",
]
[[package]]
name = "zlib-rs"
version = "0.6.8"
+2 -2
View File
@@ -76,6 +76,7 @@ trash = "5"
blake3 = "1"
dunce = "1"
dirs = "6"
zip = { version = "9", default-features = false, features = ["deflate"] }
# search, index, language
grep-searcher = "0.1"
@@ -96,9 +97,8 @@ reqwest = { version = "0.13", default-features = false, features = ["rustls-no-p
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12", "logging"] }
rustls-platform-verifier = "0.7"
sqlx = { version = "0.9", default-features = false, features = ["runtime-tokio", "tls-rustls-ring-webpki", "sqlite", "postgres", "mysql"] }
tiberius = { version = "0.12", default-features = false, features = ["tds73", "rustls", "sql-browser-tokio"] }
tiberius = { version = "0.13", default-features = false, features = ["tds73", "rustls", "sql-browser-tokio"] }
bb8 = "0.9"
bb8-tiberius = { version = "0.16", default-features = false, features = ["tds73", "tls", "with-tokio", "sql-browser"] }
uuid = { version = "1", features = ["v4"] }
# testing
+1 -1
View File
@@ -18,7 +18,7 @@ futures.workspace = true
sqlx.workspace = true
tiberius.workspace = true
bb8.workspace = true
bb8-tiberius.workspace = true
tokio-util = { workspace = true, features = ["compat"] }
dirs.workspace = true
ide-ssh.workspace = true
+47 -15
View File
@@ -47,13 +47,15 @@ pub struct QueryResult {
pub execution_ms: f64,
}
// bb8-tiberius never marks connections broken; keep an empty slot when discarded.
pub struct MssqlConnectionManager(bb8_tiberius::ConnectionManager);
// Keep an empty slot when a connection is discarded after cancellation or rollback failure.
pub struct MssqlConnectionManager(tiberius::Config);
pub struct MssqlConnection(Option<bb8_tiberius::rt::Client>);
type MssqlClient = tiberius::Client<tokio_util::compat::Compat<tokio::net::TcpStream>>;
pub struct MssqlConnection(Option<MssqlClient>);
impl std::ops::Deref for MssqlConnection {
type Target = bb8_tiberius::rt::Client;
type Target = MssqlClient;
fn deref(&self) -> &Self::Target {
self.0.as_ref().expect("live SQL Server connection")
}
@@ -67,10 +69,26 @@ impl std::ops::DerefMut for MssqlConnection {
impl ManageConnection for MssqlConnectionManager {
type Connection = MssqlConnection;
type Error = bb8_tiberius::Error;
type Error = tiberius::error::Error;
async fn connect(&self) -> Result<Self::Connection, Self::Error> {
self.0.connect().await.map(|c| MssqlConnection(Some(c)))
use tokio_util::compat::TokioAsyncWriteCompatExt;
let tcp = tokio::net::TcpStream::connect(self.0.get_addr()).await?;
tcp.set_nodelay(true)?;
let client = match MssqlClient::connect(self.0.clone(), tcp.compat_write()).await {
// Preserve the single redirect supported by the previous pool adapter.
Err(tiberius::error::Error::Routing { host, port }) => {
let mut config = self.0.clone();
config.host(host);
config.port(port);
let tcp = tokio::net::TcpStream::connect(config.get_addr()).await?;
tcp.set_nodelay(true)?;
MssqlClient::connect(config, tcp.compat_write()).await?
}
result => result?,
};
Ok(MssqlConnection(Some(client)))
}
async fn is_valid(&self, conn: &mut Self::Connection) -> Result<(), Self::Error> {
@@ -120,9 +138,8 @@ pub(crate) fn db_err(e: sqlx::Error) -> IdeError {
}
}
pub(crate) fn mssql_err(e: impl Into<bb8_tiberius::Error>) -> IdeError {
match e.into() {
bb8_tiberius::Error::Tiberius(e) => match &e {
pub(crate) fn mssql_err(e: tiberius::error::Error) -> IdeError {
match &e {
tiberius::error::Error::Server(d) => {
let code = d.code();
// Wrong login: SQL Server 18456.
@@ -136,12 +153,10 @@ pub(crate) fn mssql_err(e: impl Into<bb8_tiberius::Error>) -> IdeError {
IdeError::new(ErrorKind::Io, e.to_string())
}
_ => IdeError::new(ErrorKind::Protocol, e.to_string()),
},
bb8_tiberius::Error::Io(e) => IdeError::new(ErrorKind::Io, e.to_string()),
}
}
fn mssql_pool_err(e: bb8::RunError<bb8_tiberius::Error>) -> IdeError {
fn mssql_pool_err(e: bb8::RunError<tiberius::error::Error>) -> IdeError {
match e {
bb8::RunError::User(e) => mssql_err(e),
bb8::RunError::TimedOut => IdeError::new(ErrorKind::Io, "timed out connecting to the database"),
@@ -364,13 +379,13 @@ macro_rules! fetch {
/// Columns, rows, rows affected, more rows beyond the limit, time of the first answer.
type Fetched = (Vec<ResultColumn>, Vec<Vec<Option<String>>>, Option<u64>, bool, Option<Instant>);
async fn mssql_command(conn: &mut bb8_tiberius::rt::Client, sql: &str) -> IdeResult<()> {
async fn mssql_command(conn: &mut MssqlClient, sql: &str) -> IdeResult<()> {
// Transaction control must be a batch: sp_executesql rejects a changed transaction count.
conn.simple_query(sql).await.map_err(mssql_err)?.into_results().await.map_err(mssql_err)?;
Ok(())
}
async fn mssql_fetch(conn: &mut bb8_tiberius::rt::Client, sql: &str, max: usize) -> IdeResult<Fetched> {
async fn mssql_fetch(conn: &mut MssqlClient, sql: &str, max: usize) -> IdeResult<Fetched> {
let mut stream = conn.simple_query(sql).await.map_err(mssql_err)?;
let first = Some(Instant::now());
let columns = stream
@@ -669,7 +684,7 @@ impl Pool {
if matches!(source.ssl, DbSslMode::Disable | DbSslMode::Prefer) {
config.trust_cert();
}
let manager = MssqlConnectionManager(bb8_tiberius::ConnectionManager::new(config));
let manager = MssqlConnectionManager(config);
let pool = bb8::Pool::builder()
.max_size(MAX)
.connection_timeout(timeout)
@@ -908,6 +923,23 @@ mod tests {
use super::*;
use crate::source::{DbOptions, DbProperty};
#[test]
fn discarded_sqlserver_connections_are_broken() {
let manager = MssqlConnectionManager(tiberius::Config::new());
assert!(manager.has_broken(&mut MssqlConnection(None)));
}
#[tokio::test]
async fn sqlserver_connect_reports_io_errors() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let mut config = tiberius::Config::new();
config.host("127.0.0.1");
config.port(listener.local_addr().unwrap().port());
drop(listener);
let result = MssqlConnectionManager(config).connect().await;
assert!(matches!(result, Err(tiberius::error::Error::Io { .. })));
}
#[test]
fn sqlserver_cells_preserve_values_and_nulls() {
use std::borrow::Cow;
+1 -1
View File
@@ -17,7 +17,7 @@ tokio.workspace = true
portable-pty.workspace = true
parking_lot.workspace = true
tracing.workspace = true
zip = { version = "=2.4.2", default-features = false, features = ["deflate"] }
zip.workspace = true
[target.'cfg(unix)'.dependencies]
libc = "0.2"
+1 -1
View File
@@ -18,7 +18,7 @@ specta.workspace = true
toml.workspace = true
tracing.workspace = true
which = "8"
zip = { version = "=2.4.2", default-features = false, features = ["deflate"] }
zip.workspace = true
[dev-dependencies]
tempfile.workspace = true
+1 -1
View File
@@ -16,7 +16,7 @@ tracing.workspace = true
which = "8"
flate2 = "1"
tar = "0.4"
zip = { version = "=2.4.2", default-features = false, features = ["deflate"] }
zip.workspace = true
sha2 = "0.10"
hex = "0.4"
+323 -5
View File
@@ -65,6 +65,7 @@ pub fn extract(
ArchiveKind::TarXz => extract_with_tar(archive, dest, "-xJf", cancel),
ArchiveKind::TarBz2 => extract_with_tar(archive, dest, "-xjf", cancel),
ArchiveKind::Gz => {
validate_single_name(dest, single_name)?;
let mut gz = flate2::read::GzDecoder::new(BufReader::new(File::open(archive)?));
let target = dest.join(single_name);
let mut out = File::create(&target)?;
@@ -72,6 +73,7 @@ pub fn extract(
make_executable(&target)
}
ArchiveKind::Raw => {
validate_single_name(dest, single_name)?;
let target = dest.join(single_name);
fs::copy(archive, &target)?;
make_executable(&target)
@@ -80,15 +82,25 @@ pub fn extract(
}
fn extract_tar<R: io::Read>(mut archive: tar::Archive<R>, dest: &Path, cancel: &CancellationToken) -> IdeResult<()> {
archive.set_preserve_permissions(true);
archive.set_preserve_permissions(false);
archive.set_overwrite(true);
let result = (|| {
for entry in archive.entries().map_err(|e| io_err("reading archive", e))? {
check(cancel)?;
let mut entry = entry.map_err(|e| io_err("reading archive", e))?;
// `unpack_in` refuses entries escaping `dest` (returns Ok(false)).
if entry.header().entry_type().is_symlink() {
let path = entry.path().map_err(|e| io_err("reading archive path", e))?;
if let Some(target) = entry.link_name().map_err(|e| io_err("reading archive link", e))? {
validate_link(dest, &path, &target)?;
}
}
// `unpack_in` refuses escaping paths and validates link ancestors before writing.
entry.unpack_in(dest).map_err(|e| io_err("extracting", e))?;
}
Ok(())
})();
let sanitized = sanitize_tree(dest, dest);
result.and(sanitized)
}
fn extract_zip(archive: &Path, dest: &Path, cancel: &CancellationToken) -> IdeResult<()> {
@@ -100,6 +112,7 @@ fn extract_zip(archive: &Path, dest: &Path, cancel: &CancellationToken) -> IdeRe
if !is_safe_relative(&rel) {
continue;
}
reject_symlink_components(dest, &rel)?;
let out = dest.join(&rel);
if file.is_dir() {
fs::create_dir_all(&out)?;
@@ -112,6 +125,7 @@ fn extract_zip(archive: &Path, dest: &Path, cancel: &CancellationToken) -> IdeRe
if file.is_symlink() {
let mut target = String::new();
io::Read::read_to_string(&mut file, &mut target)?;
validate_link(dest, &rel, Path::new(&target))?;
let _ = fs::remove_file(&out);
std::os::unix::fs::symlink(target, &out)?;
continue;
@@ -132,14 +146,121 @@ fn extract_zip(archive: &Path, dest: &Path, cancel: &CancellationToken) -> IdeRe
fn extract_with_tar(archive: &Path, dest: &Path, flags: &str, cancel: &CancellationToken) -> IdeResult<()> {
let tar = which::which("tar").map_err(|_| IdeError::unavailable("`tar` is required to unpack this archive"))?;
let mut cmd = std::process::Command::new(tar);
cmd.arg(flags).arg(archive).arg("-C").arg(dest);
let (ok, output, _) = crate::process::run_collect(cmd, cancel, &|_| {})?;
cmd.arg(flags).arg(archive).arg("--no-same-owner").arg("--no-same-permissions").arg("-C").arg(dest);
let result = crate::process::run_collect(cmd, cancel, &|_| {});
// System tar implementations differ in which special permission bits they retain.
// Sanitize partial extractions too, without following archive-created links.
let sanitized = sanitize_tree(dest, dest);
let (ok, output, _) = result?;
sanitized?;
if !ok {
return Err(io_err("tar failed", output.trim()));
}
Ok(())
}
fn validate_single_name(dest: &Path, name: &str) -> IdeResult<()> {
let path = Path::new(name);
if !is_safe_relative(path) {
return Err(IdeError::invalid("archive filename escapes its destination"));
}
reject_symlink_components(dest, path)
}
fn reject_symlink_components(dest: &Path, rel: &Path) -> IdeResult<()> {
let mut path = dest.to_path_buf();
for component in rel.components() {
path.push(component);
match fs::symlink_metadata(&path) {
Ok(meta) if meta.file_type().is_symlink() => {
return Err(IdeError::invalid("archive entry traverses a symlink"));
}
Ok(_) => {}
Err(e) if e.kind() == io::ErrorKind::NotFound => {}
Err(e) => return Err(e.into()),
}
}
Ok(())
}
fn validate_link(root: &Path, path: &Path, target: &Path) -> IdeResult<()> {
fn resolve(root: &Path, path: &Path, resolved: &mut PathBuf, links: &mut usize) -> IdeResult<()> {
for component in path.components() {
match component {
Component::Normal(name) => {
resolved.push(name);
match fs::symlink_metadata(root.join(&*resolved)) {
Ok(meta) if meta.file_type().is_symlink() => {
*links += 1;
if *links > 40 {
return Err(IdeError::invalid("archive symlink cycle"));
}
let target = fs::read_link(root.join(&*resolved))?;
resolved.pop();
resolve(root, &target, resolved, links)?;
}
Ok(_) => {}
Err(e) if e.kind() == io::ErrorKind::NotFound => {}
Err(e) => return Err(e.into()),
}
}
Component::CurDir => {}
Component::ParentDir if resolved.pop() => {}
_ => return Err(IdeError::invalid("archive symlink escapes its destination")),
}
}
Ok(())
}
let mut resolved = PathBuf::new();
let mut links = 0;
resolve(root, path.parent().unwrap_or(Path::new("")), &mut resolved, &mut links)?;
resolve(root, target, &mut resolved, &mut links)
}
fn sanitize_tree(root: &Path, dir: &Path) -> IdeResult<()> {
#[cfg(unix)]
let mode = {
use std::os::unix::fs::PermissionsExt;
let mode = fs::metadata(dir)?.permissions().mode() & 0o777;
// Even a mode-000 directory may contain files whose special bits need clearing.
fs::set_permissions(dir, fs::Permissions::from_mode(mode | 0o700))?;
mode
};
let mut error = None;
for entry in fs::read_dir(dir)? {
let result = (|| {
let path = entry?.path();
let meta = fs::symlink_metadata(&path)?;
if meta.file_type().is_symlink() {
let target = fs::read_link(&path)?;
if let Err(e) = validate_link(root, path.strip_prefix(root).expect("entry below root"), &target) {
fs::remove_file(&path)?;
return Err(e);
}
} else {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&path, fs::Permissions::from_mode(meta.permissions().mode() & 0o777))?;
}
if meta.is_dir() {
sanitize_tree(root, &path)?;
}
}
Ok(())
})();
if let Err(e) = result {
error.get_or_insert(e);
}
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(dir, fs::Permissions::from_mode(mode))?;
}
error.map_or(Ok(()), Err)
}
fn is_safe_relative(p: &Path) -> bool {
p.components().all(|c| matches!(c, Component::Normal(_) | Component::CurDir))
}
@@ -149,7 +270,7 @@ pub fn make_executable(path: &Path) -> IdeResult<()> {
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(path)?.permissions();
perms.set_mode(perms.mode() | 0o755);
perms.set_mode((perms.mode() & 0o777) | 0o755);
fs::set_permissions(path, perms)?;
}
#[cfg(not(unix))]
@@ -352,6 +473,203 @@ mod tests {
assert!(dest.join("ok.txt").exists());
}
#[test]
fn zip_symlinks_are_confined_to_the_destination() {
for target in ["../outside", "/tmp/outside", "a/../../outside"] {
let tmp = tempfile::tempdir().unwrap();
let archive = tmp.path().join("evil.zip");
let mut z = zip::ZipWriter::new(File::create(&archive).unwrap());
z.add_symlink("link", target, zip::write::SimpleFileOptions::default()).unwrap();
z.finish().unwrap();
let dest = tmp.path().join("out");
assert!(extract(&archive, ArchiveKind::Zip, &dest, "x", &CancellationToken::new()).is_err());
assert!(fs::symlink_metadata(dest.join("link")).is_err());
}
// A link inside the archive (e.g. a framework's Versions/Current) still extracts.
let tmp = tempfile::tempdir().unwrap();
let archive = tmp.path().join("ok.zip");
let mut z = zip::ZipWriter::new(File::create(&archive).unwrap());
z.start_file("Versions/A/lib", zip::write::SimpleFileOptions::default()).unwrap();
io::Write::write_all(&mut z, b"x").unwrap();
z.add_symlink("Versions/Current", "A", zip::write::SimpleFileOptions::default()).unwrap();
z.finish().unwrap();
let dest = tmp.path().join("out");
extract(&archive, ArchiveKind::Zip, &dest, "x", &CancellationToken::new()).unwrap();
assert_eq!(fs::read_link(dest.join("Versions/Current")).unwrap(), Path::new("A"));
assert_eq!(fs::read(dest.join("Versions/Current/lib")).unwrap(), b"x");
}
#[cfg(unix)]
#[test]
fn zip_does_not_write_or_chmod_through_existing_symlinks() {
use std::os::unix::fs::{PermissionsExt, symlink};
for name in ["link", "dir/file"] {
let tmp = tempfile::tempdir().unwrap();
let outside = tmp.path().join("outside");
fs::create_dir(&outside).unwrap();
let file = outside.join("file");
fs::write(&file, "original").unwrap();
fs::set_permissions(&file, fs::Permissions::from_mode(0o600)).unwrap();
let dest = tmp.path().join("out");
fs::create_dir(&dest).unwrap();
symlink(&file, dest.join("link")).unwrap();
symlink(&outside, dest.join("dir")).unwrap();
let archive = tmp.path().join("evil.zip");
zip_file(&archive, &[(name, b"replaced", 0o777)]);
assert!(extract(&archive, ArchiveKind::Zip, &dest, "x", &CancellationToken::new()).is_err());
assert_eq!(fs::read_to_string(&file).unwrap(), "original");
assert_eq!(fs::metadata(&file).unwrap().permissions().mode() & 0o777, 0o600);
}
}
#[cfg(unix)]
#[test]
fn archives_strip_special_permission_bits() {
use std::os::unix::fs::PermissionsExt;
let tmp = tempfile::tempdir().unwrap();
for kind in [ArchiveKind::TarGz, ArchiveKind::Zip, ArchiveKind::TarXz, ArchiveKind::TarBz2] {
let archive = tmp.path().join(format!("{kind:?}.archive"));
match kind {
ArchiveKind::TarGz => tar_gz(&archive, &[("tool", b"binary", 0o7777)]),
ArchiveKind::Zip => {
zip_file(&archive, &[("tool", b"binary", 0o777)]);
// ZipWriter strips special bits, so set the central directory's Unix mode directly.
let mut bytes = fs::read(&archive).unwrap();
let central = bytes.windows(4).position(|w| w == b"PK\x01\x02").unwrap();
bytes[central + 38..central + 42].copy_from_slice(&(0o107777_u32 << 16).to_le_bytes());
fs::write(&archive, bytes).unwrap();
}
_ => {
let source = tmp.path().join(format!("source-{kind:?}"));
fs::create_dir(&source).unwrap();
fs::write(source.join("tool"), "binary").unwrap();
fs::set_permissions(source.join("tool"), fs::Permissions::from_mode(0o7777)).unwrap();
let flag = if kind == ArchiveKind::TarXz { "-cJf" } else { "-cjf" };
assert!(
std::process::Command::new("tar")
.arg(flag)
.arg(&archive)
.arg("-C")
.arg(&source)
.arg("tool")
.status()
.unwrap()
.success()
);
}
}
let dest = tmp.path().join(format!("out-{kind:?}"));
extract(&archive, kind, &dest, "x", &CancellationToken::new()).unwrap();
let mode = fs::metadata(dest.join("tool")).unwrap().permissions().mode();
assert_eq!(mode & 0o7000, 0, "{kind:?}");
assert_ne!(mode & 0o111, 0, "{kind:?}");
}
}
#[test]
fn tar_entries_cannot_escape() {
for (name, link) in [("../escape", None), ("link", Some("../escape"))] {
let tmp = tempfile::tempdir().unwrap();
let archive = tmp.path().join("evil.tar.gz");
let gz = flate2::write::GzEncoder::new(File::create(&archive).unwrap(), flate2::Compression::fast());
let mut builder = tar::Builder::new(gz);
let mut header = tar::Header::new_gnu();
header.as_mut_bytes()[..name.len()].copy_from_slice(name.as_bytes());
header.set_mode(0o7777);
header.set_size(0);
if let Some(target) = link {
header.set_entry_type(tar::EntryType::Symlink);
header.set_link_name(target).unwrap();
}
header.set_cksum();
builder.append(&header, io::empty()).unwrap();
builder.into_inner().unwrap().finish().unwrap();
let dest = tmp.path().join("out");
let _ = extract(&archive, ArchiveKind::TarGz, &dest, "x", &CancellationToken::new());
assert!(!tmp.path().join("escape").exists());
assert!(fs::symlink_metadata(dest.join("link")).is_err());
}
}
#[cfg(unix)]
#[test]
fn compressed_tar_cannot_escape_or_retain_special_bits() {
use std::os::unix::fs::PermissionsExt;
// Both fixtures contain link -> ../outside, link/victim, ../escaped, and a mode-7777 file.
for (kind, bytes) in [
(ArchiveKind::TarXz, include_bytes!("../tests/fixtures/escape.tar.xz").as_slice()),
(ArchiveKind::TarBz2, include_bytes!("../tests/fixtures/escape.tar.bz2").as_slice()),
] {
let tmp = tempfile::tempdir().unwrap();
fs::create_dir(tmp.path().join("outside")).unwrap();
let victim = tmp.path().join("outside/victim");
fs::write(&victim, "original").unwrap();
fs::set_permissions(&victim, fs::Permissions::from_mode(0o600)).unwrap();
let archive = tmp.path().join("evil.archive");
fs::write(&archive, bytes).unwrap();
let dest = tmp.path().join("out");
assert!(extract(&archive, kind, &dest, "x", &CancellationToken::new()).is_err());
assert_eq!(fs::read_to_string(&victim).unwrap(), "original");
assert_eq!(fs::metadata(&victim).unwrap().permissions().mode() & 0o7777, 0o600);
assert!(!tmp.path().join("escaped").exists());
assert!(fs::symlink_metadata(dest.join("link")).is_err());
if let Ok(meta) = fs::metadata(dest.join("special")) {
assert_eq!(meta.permissions().mode() & 0o7000, 0);
}
}
}
#[cfg(unix)]
#[test]
fn tar_link_resolution_handles_chains_and_dangling_targets() {
use std::os::unix::fs::symlink;
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
fs::create_dir(root.join("lib")).unwrap();
symlink(".", root.join("alias")).unwrap();
assert!(validate_link(root, Path::new("link"), Path::new("alias/../missing")).is_err());
assert!(validate_link(root, Path::new("lib/link"), Path::new("../missing")).is_ok());
symlink("cycle", root.join("cycle")).unwrap();
assert!(validate_link(root, Path::new("link"), Path::new("cycle")).is_err());
}
#[cfg(unix)]
#[test]
fn sanitizing_clears_all_modes_even_after_bad_links() {
use std::os::unix::fs::{PermissionsExt, symlink};
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
for name in ["a", "b"] {
let dir = root.join(name);
fs::create_dir(&dir).unwrap();
symlink("../../outside", dir.join("bad-link")).unwrap();
fs::write(dir.join("tool"), "binary").unwrap();
fs::set_permissions(dir.join("tool"), fs::Permissions::from_mode(0o7777)).unwrap();
fs::set_permissions(&dir, fs::Permissions::from_mode(0o7000)).unwrap();
}
fs::set_permissions(root, fs::Permissions::from_mode(0o7777)).unwrap();
assert!(sanitize_tree(root, root).is_err());
assert_eq!(fs::metadata(root).unwrap().permissions().mode() & 0o7000, 0);
for name in ["a", "b"] {
let dir = root.join(name);
assert_eq!(fs::metadata(&dir).unwrap().permissions().mode() & 0o7777, 0);
fs::set_permissions(&dir, fs::Permissions::from_mode(0o700)).unwrap();
assert!(fs::symlink_metadata(dir.join("bad-link")).is_err());
assert_eq!(fs::metadata(dir.join("tool")).unwrap().permissions().mode() & 0o7000, 0);
}
}
#[test]
fn single_payload_names_cannot_escape() {
let tmp = tempfile::tempdir().unwrap();
let archive = tmp.path().join("raw");
fs::write(&archive, "payload").unwrap();
for kind in [ArchiveKind::Raw, ArchiveKind::Gz] {
assert!(extract(&archive, kind, &tmp.path().join("out"), "../escaped", &CancellationToken::new()).is_err());
assert!(!tmp.path().join("escaped").exists());
}
}
#[test]
fn single_gzip_and_raw_payloads() {
let tmp = tempfile::tempdir().unwrap();
Binary file not shown.
Binary file not shown.
+27
View File
@@ -0,0 +1,27 @@
"""Regenerate the malicious tar fixtures used by archive.rs tests (Python stdlib only)."""
import bz2
import io
import lzma
from pathlib import Path
import tarfile
archive = io.BytesIO()
with tarfile.open(fileobj=archive, mode="w", format=tarfile.GNU_FORMAT) as tar:
for name, target, data in [
("link", "../outside", b""),
("link/victim", None, b"replaced"),
("../escaped", None, b"escaped"),
("special", None, b"special"),
]:
entry = tarfile.TarInfo(name)
entry.mode = 0o7777
if target:
entry.type = tarfile.SYMTYPE
entry.linkname = target
else:
entry.size = len(data)
tar.addfile(entry, io.BytesIO(data))
for extension, compress in [("xz", lzma.compress), ("bz2", bz2.compress)]:
Path(__file__).with_name(f"escape.tar.{extension}").write_bytes(compress(archive.getvalue()))
+1 -1
View File
@@ -19,7 +19,7 @@ chardetng.workspace = true
trash.workspace = true
tracing.workspace = true
parking_lot.workspace = true
zip = { version = "=2.4.2", default-features = false, features = ["deflate"] }
zip.workspace = true
[dev-dependencies]
tempfile.workspace = true
+2 -1
View File
@@ -120,7 +120,8 @@ impl Archive {
nodes.insert(String::new(), Node { dir: true, size: 0, index: None, children: Vec::new() });
for i in 0..zip.len() {
let Ok(file) = zip.by_index_raw(i) else { continue };
let Some(name) = clean_name(file.name()) else { continue };
let Ok(name) = file.name() else { continue };
let Some(name) = clean_name(&name) else { continue };
let (dir, size) = (file.is_dir(), file.size());
drop(file);
let mut parent = parent_of(&name).0;
+1 -1
View File
@@ -23,4 +23,4 @@ globset.workspace = true
[dev-dependencies]
tempfile.workspace = true
encoding_rs.workspace = true
zip = { version = "=2.4.2", default-features = false, features = ["deflate"] }
zip.workspace = true
+26
View File
@@ -0,0 +1,26 @@
[advisories]
version = 2
unmaintained = "workspace"
yanked = "warn"
ignore = [
# SQLx MySQL uses RSA public-key encryption; russh/ssh-key use signatures.
# No RSA private-key decryption is exposed to attacker-controlled ciphertext.
{ id = "RUSTSEC-2023-0071", reason = "RSA is used for MySQL password encryption and SSH signatures, not RSA decryption" },
]
[licenses]
allow = [
"MIT", "Apache-2.0", "Apache-2.0 WITH LLVM-exception", "BSD-2-Clause",
"BSD-3-Clause", "ISC", "Zlib", "0BSD", "CC0-1.0", "Unlicense",
# File-level copyleft, used unmodified as dependencies.
"MPL-2.0",
"Unicode-3.0", "Unicode-DFS-2016", "CDLA-Permissive-2.0", "OpenSSL",
]
[bans]
multiple-versions = "warn"
[sources]
unknown-registry = "deny"
unknown-git = "deny"
allow-registry = ["https://github.com/rust-lang/crates.io-index"]