Security Hardening Report: QRE Privacy Toolkit v2.6.9
With every release, we conduct a thorough security review of the entire stack. This post documents the hardening work completed for qre-gui v2.6.9 (Tauri V2 + React/TypeScript + Rust), from dependency auditing all the way through to fuzz testing infrastructure. Transparency is a core value for a security tool, so we are publishing the full findings here.
1. Dependency Security
rand 0.8 → 0.9 Migration
We upgraded rand and rand_chacha to
version 0.9 across the entire project, eliminating one of three
duplicate rand versions in the dependency tree. A
residual rand 0.7.3 copy remains as a deep transitive
dependency inside Tauri itself and cannot be controlled from our
side.
The migration required adapting to a breaking API change:
OsRng no longer implements
RngCore directly in 0.9 — it now implements
TryRngCore because OS random number generation can
theoretically fail. Every OsRng.fill_bytes() call
across crypto.rs, crypto_stream.rs, and
keychain.rs was updated to use
.try_fill_bytes(...).expect("OS RNG failed"). The
.expect() is intentional: if the OS random number
generator is unavailable during key or nonce generation, there is no
safe fallback and the operation must abort immediately.
A version conflict in keychain.rs was resolved by
aliasing the two incompatible OsRng types, since
argon2 re-exports an older rand_core 0.6
snapshot internally:
use argon2::password_hash::{rand_core::OsRng as Argon2OsRng, SaltString};
use rand::{rngs::OsRng, TryRngCore};
cargo-audit — Advisory Review
Running cargo audit produced 19 warnings and 0 errors.
All warnings were triaged: the GTK3 bindings (10 crates),
unic-* crates (5 crates),
proc-macro-error, and fxhash are all
transitive Tauri dependencies and were deferred to upstream.
The one advisory worth calling out is
RUSTSEC-2024-0429 for glib — an
unsound Iterator implementation buried inside
webkit2gtk internals. It is not directly callable from
application code, but is flagged for monitoring until a Tauri
release integrates glib 0.19+.
bincode 1.3.3 — a direct project dependency
— is flagged as unmaintained and left as an active error in
deny.toml as a deliberate reminder to migrate to
bincode v2.
cargo-deny — License Compliance
658 license errors and 2 unlicense warnings were resolved. The
package was marked
license = "proprietary", and the
deny.toml allowlist was expanded to cover the full
Tauri/Rust ecosystem: MIT, Apache-2.0, ISC, BSD-2-Clause,
BSD-3-Clause, Unicode-3.0, MPL-2.0, OpenSSL, Zlib, CC0-1.0, and
others. A [[licenses.clarify]] block was added for the
ring TLS crypto library, which bundles its license
non-standardly and cannot be auto-detected.
2. Rust Backend — Clippy Hardening
All 20 Clippy errors were resolved across 9 files, with
cargo clippy -- -D warnings now exiting clean. The most
security-relevant fix was in utils.rs: the
set_readonly(false) call was replaced with a
platform-gated implementation. On Unix, file permissions are now set
explicitly to 0o600 (owner read/write only) via
PermissionsExt. Other fixes across the codebase
involved collapsing nested matches, simplifying boolean logic,
removing needless borrows, and fixing range patterns.
3. Dead Code Removal
Two dead modules were identified and deleted from the Rust backend:
-
entropy.rs— contained a blocking stdin read (io::stdin().read_line()) that is architecturally impossible to call in a GUI application. -
secure_rng.rs— aChaCha20wrapper never called from production code. Its two unit tests were migrated totests.rsbefore deletion.
Two files were also flagged for removal from version control:
src/components/layout/Sidebar.css (orphaned stylesheet)
and tauri.conf.json.backup (a backup configuration file
that should never be committed to a repository).
4. Eliminating Panics in Production Code
A full grep across all source files identified every
.unwrap(), .expect(),
panic!(), unimplemented!(), and
unreachable!() call. These were sorted into two
categories: legitimate (test code, truly unrecoverable startup
failures, hardcoded regex) and production code that needed fixing.
The most significant fixes were 17 mutex panics across
commands/files.rs and commands/vault.rs.
Every .lock().unwrap() was replaced with:
state.master_key.lock().unwrap_or_else(|e| e.into_inner())
A poisoned mutex (caused by a thread panicking while holding the
lock) would previously cause every subsequent caller to panic
forever. .into_inner() recovers the data safely. Eight
.parent().unwrap() path calls and nine
path.to_str().unwrap()
UTF-8 path calls were similarly converted to propagate clean errors
to the frontend rather than crashing the backend.
5. Filesystem Scope Hardening
This was the most impactful security fix in the release. The
original Tauri
default.json contained "**" in the
fs:scope allow list, granting the application
unrestricted read/write access to the entire filesystem —
including system directories, SSH keys, browser profiles, and
credential stores.
The fix replaces "**" with "$HOME/**" for
all user content, and adds an explicit deny list
blocking the most sensitive subdirectories even within the home
folder:
"deny": [
"$HOME/.ssh/**",
"$HOME/.gnupg/**",
"$HOME/.aws/**",
"$HOME/.config/google-chrome/**",
"$HOME/.mozilla/**",
"$HOME/AppData/Roaming/Microsoft/**",
"$HOME/AppData/Local/Microsoft/**",
"$HOME/Library/Keychains/**",
"$HOME/Library/Passwords/**"
]
It is worth emphasising that the Tauri capability scope is enforced
in Rust on the IPC bridge — it
cannot be bypassed by JavaScript running in the
WebView, regardless of XSS, malicious npm packages, or compromised
dependencies. The frontend isDangerousPath()
blacklist is retained as a UX layer only and is not relied upon as a
security boundary.
6. Frontend Fixes
Two frontend issues were resolved. In VaultView.tsx,
the password input field in the vault edit modal was missing
type="password", causing entered passwords to be
displayed in plaintext. An Eye/EyeOff toggle was added using icons
already present in the project.
In useFileSystem.ts, platform() was being
called 5 times across the hook in inconsistent contexts. The result
is now cached once at hook initialisation via useRef.
This also fixed a bug where navigating to "/" on Linux
or macOS would call the Windows drive enumeration command, returning
an empty view with no way to navigate back.
7. Fuzz Testing Infrastructure
A fuzz target was created at
src-tauri/fuzz/fuzz_targets/fuzz_decrypt.rs targeting
the decrypt pipeline — the highest-value attack surface since
it processes untrusted, user-provided file bytes. The target
exercises three attack surfaces in a single pass:
- Malformed bincode — adversarially crafted container deserialization
- Invalid ciphertext — AES-GCM authentication tag verification with random data
- Decompression safety — zstd payloads that could exhaust memory
cargo-fuzz requires Linux and Rust nightly. A GitHub
Actions workflow was added to run fuzzing automatically on a weekly
schedule against ubuntu-latest, keeping it independent
of the development platform.
Remaining Action Items
The following items were identified but not resolved in this session and are carried forward as tracked work:
-
[High] Migrate
bincode1.x → 2.x (direct dep, flagged unmaintained) -
[High] Monitor
glibRUSTSEC-2024-0429 and update Tauri when patched -
[Medium] Add
proptestroundtrip invariants to complement fuzz testing -
[Low] Audit
os:defaultcapability for environment variable exposure -
[Low] Align
randversions with Tauri to eliminate therand 0.7.3duplicate