48 KiB
Introduction
This audit is a basic pre-release security review of LibreQoS as of 2026-05-15. It focuses on dependency advisories, Rust dependency hygiene, control-plane exposure, bridged-interface and eBPF malformed-traffic handling, panic/error/type-loss paths, and Node Manager privacy, authentication, and XSS risk.
The review is static unless a section explicitly says otherwise. It does not cover Ubuntu/Linux distribution vulnerabilities, live firewall state, NIC driver behavior, or private deployment-specific controls that are not visible in this checkout.
Executive summary
- Overall grade: C. LibreQoS has several strong security foundations, but the current release state includes multiple actionable control-plane and WebUI issues that should be fixed or explicitly risk-accepted before release.
- No CVE-class Rust dependency vulnerability was confirmed as reachable during
the dependency audit. The
randsoundness advisory has been remediated by updating the locked 0.8 dependency line torand 0.8.6, and one likely unused dependency should be removed. - The Caddy/TLS path is generally well-directed for the WebUI, but the sibling API service can still bind directly on all interfaces, malformed bearer headers can panic API authentication, WebUI/local API CORS is too broad, and WebUI login lacks rate limiting.
- The eBPF packet parsers show good verifier-conscious bounds checking for common encapsulation paths, but malformed traffic can still create operational DoS or visibility loss through packet-rate debug logging, IPv4 fragment/header edge cases, and non-LRU map exhaustion.
- The panic/type-loss review found request-time panic paths and unchecked numeric narrowing that can lose operational data in queue stats and NetFlow export.
- Node Manager has the highest concentration of release-relevant WebUI risk:
anonymous/read-only paths can retrieve raw subscriber and topology data,
operator-controlled strings reach
innerHTML, the session cookie is readable by JavaScript, and read-only users can write dashboard themes that can become stored XSS. - Recommended release priority is to first fix WebUI XSS/session-cookie exposure, server-side anonymization for anonymous/demo mode, API direct-listener/TLS bypass risk, malformed auth-header panic, and production packet-path debug logging.
Rust dependency audit
Date: 2026-05-15
Scope: Rust workspace under src/rust/, using src/rust/Cargo.toml and
src/rust/Cargo.lock.
Commands run:
cargo auditcargo machetecargo tree --locked --workspace --depth 1cargo update -p rand@0.8.5 --precise 0.8.6cargo tree --locked -p lqosd -e featurescargo check -p lqos_probe -p lqosd
Summary
cargo auditscanned 549 locked crate dependencies.- No CVE-class vulnerability was reported.
- The previous
randsoundness advisory no longer appears incargo auditafter updatingrand 0.8.5torand 0.8.6. - Direct
randimports now disable default features and re-enable onlystdandstd_rng, which are required for the currentthread_rngandrandomcalls. The resolved graph still enablesrand/defaultthrough upstreamtungsteniteandcookiedependencies, so feature minimization is limited to LibreQoS' direct imports unless those upstream crates are patched or upgraded. cargo machetefound one likely unused dependency:tokioinlqos_network_devices.- The remaining
cargo auditwarnings were maintenance-only notices and are not counted as security issues here.
Dependency findings and triage notes
Resolved: RUSTSEC-2026-0097 / GHSA-cq8v-f236-94qc rand soundness advisory
Paths importing or using the affected dependency/API:
src/rust/lqosd/Cargo.tomlimportsrand = { version = "0.8.6", default-features = false, features = ["std", "std_rng"] }.src/rust/lqosd/src/node_manager/auth.rsusesrand::thread_rng().fill_bytes(...)for session-key generation.src/rust/lqosd/src/node_manager/ws/single_user_channels.rsusesrand::random::<u64>()for chatbot request IDs.src/rust/lqos_probe/Cargo.tomlimportsrand = { version = "0.8.6", default-features = false, features = ["std", "std_rng"] }.src/rust/lqos_probe/src/lib.rsusesrand::randomfor ICMP ping IDs.src/rust/lqosd/Cargo.tomlalso importstungstenite,tokio-tungstenite, andaxum-extra, which appear in thecargo auditdependency tree aboverand 0.8.6.
Short description:
rand versions including 0.8.5 have a soundness advisory involving
rand::thread_rng / rand::rng. The trigger requires the log and
thread_rng features, a custom logger, that logger calling the RNG, and the RNG
reseeding while called from the logger. LibreQoS has affected API calls, but the
audit did not find a custom log::Log implementation or log::set_logger path
in src/rust/, so the full trigger path was not found.
Remediation:
- Updated the locked 0.8 line with
cargo update -p rand@0.8.5 --precise 0.8.6. - Minimized LibreQoS' direct
randfeature requests tostdandstd_rng. - Re-ran
cargo audit; therandadvisory is no longer reported. - Re-ran
cargo check -p lqos_probe -p lqosd; both crates compile with the updated dependency.
Unused dependency: tokio in lqos_network_devices
Path:
src/rust/lqos_network_devices/Cargo.tomlimportstokio = { workspace = true }.
Short description:
cargo machete reports tokio as unused in lqos_network_devices. A source
search found no tokio references under src/rust/lqos_network_devices/.
This is not a security issue, but it is avoidable dependency surface.
Recommended actions:
- Remove the
tokiodependency fromsrc/rust/lqos_network_devices/Cargo.toml. - Run
cargo check -p lqos_network_devices. - Re-run
cargo macheteto confirm the dependency list is clean.
Non-security audit warnings
The following cargo audit results are maintenance-only warnings, not security
findings under this audit policy:
bincode 1.3.3is unmaintained; imported bylqosd.fxhash 0.2.1is unmaintained; imported bylqosdandlqos_network_devices.paste 1.0.15is unmaintained; transitive throughdefault-net/netlink-packet-*.serde_cbor 0.11.2is unmaintained; imported by several LibreQoS crates andlqos_bus.
These should be tracked as modernization work, not listed as release security findings unless a separate vulnerability is reported for them.
Network control-plane audit
Date: 2026-05-15
Scope assumptions:
- The Caddy / SSL / TLS option is installed.
- Linux, Ubuntu, kernel, and distribution package vulnerabilities are out of scope for this section.
- The control interface is in scope. The two bridge interfaces, whether XDP or Linux bridge backed by eBPF, are out of scope for this section.
- An open listener is not a vulnerability by itself. This section looks for exploitability, unauthorized access, credential exposure, authorization bypass, or remotely triggerable failure on the control plane.
- The sibling
../../lqos_api/repo is included because it is exposed behind the managed Caddy configuration.
Files and directories reviewed:
docs/v2.0/https-caddy.mddocs/v2.0/api.mdsrc/rust/lqos_setup/src/ssl.rssrc/rust/lqos_setup/src/web.rssrc/rust/lqosd/src/node_manager/run.rssrc/rust/lqosd/src/node_manager/auth.rssrc/rust/lqosd/src/node_manager/local_api.rssrc/rust/lqosd/src/node_manager/static_pages.rssrc/rust/lqos_config/src/authentication.rs../../lqos_api/src/main.rs../../lqos_api/src/web.rs../../lqos_api/src/web_security.rs../../lqos_api/README.md
Summary
- The managed Caddy configuration disables the Caddy admin API, proxies the
WebUI to
127.0.0.1:9123, and proxies/api/v1/*to127.0.0.1:9122. - The WebUI runtime listener is configured to move to loopback for the Caddy path. That loopback listener is not a finding.
- The sibling
lqos_apiservice still binds directly to:::9122. If that port is reachable on the control interface, authenticated API traffic can bypass the Caddy/TLS path. Runtime reachability was not verified in this code audit. - Three control-plane findings and one reachability-unknown exposure are listed below. Public API documentation and the explicit anonymous read-only demo mode are recorded as observations, not findings by themselves.
Findings
Reachability unknown: direct lqos_api listener can bypass the Caddy/TLS path
Path:
../../lqos_api/src/main.rs
Short description:
lqos_api binds its HTTP server to :::9122. The managed Caddy configuration
proxies API traffic to 127.0.0.1:9122, but the API process itself also remains
able to listen on all interfaces unless deployment firewalling blocks it.
Exposure / threat:
The API binds all interfaces while Caddy proxies the intended HTTPS path to
localhost. If port 9122 is reachable on the control interface, a client can
send the x-bearer credential over direct HTTP instead of the Caddy-protected
HTTPS path. This audit verified the code-level listener and Caddy upstream, but
did not verify runtime firewall or socket exposure on an installed host.
Recommended actions:
- Make the
lqos_apilisten address configurable and default it to127.0.0.1:9122when the Caddy option is installed. - Update the Caddy/setup integration and API documentation so remote operators
use only the HTTPS
/api/v1/path. - Add install-time firewall guidance or service hardening that blocks direct
control-interface access to
9122when Caddy is enabled.
Malformed x-bearer header can panic API authentication
Path:
../../lqos_api/src/web_security.rs
Short description:
The API authentication middleware calls header.to_str().unwrap() while
processing the unauthenticated x-bearer header.
Exposure / threat:
A remote client can send a malformed header value that fails UTF-8 conversion. Authentication should reject that request, but the current code can panic while handling unauthenticated input. Even if Axum/Tokio limits the blast radius to a request task or connection, this is a remotely triggerable control-plane failure path.
Recommended actions:
- Replace the
unwrap()with explicit error handling that returns401 Unauthorizedfor invalid or missing bearer headers. - Keep malformed authentication input on the same path as other auth failures: no panic, no stack trace, and no different response body that helps probing.
- After fixing, add a small test for a non-UTF-8 or otherwise invalid
x-bearervalue.
WebUI and local API use credentialed permissive CORS
Paths:
src/rust/lqosd/src/node_manager/run.rssrc/rust/lqosd/src/node_manager/static_pages.rssrc/rust/lqosd/src/node_manager/local_api.rssrc/rust/lqosd/src/node_manager/auth.rs
Short description:
The WebUI and local API install CorsLayer::very_permissive(), a permissive
credentialed CORS policy. The WebUI session uses a User-Token cookie with
SameSite=Lax, but without Secure or HttpOnly.
Exposure / threat:
I did not find a documented cross-origin WebUI client in the reviewed code. For
a cookie-authenticated control-plane UI, permissive credentialed CORS grants
browser read access to origins outside the WebUI's own origin whenever the
browser sends the User-Token cookie. SameSite=Lax limits common
unrelated-site subresource requests, but this policy is still broader than the
reviewed WebUI needs. The local API and WebUI should not grant credentialed
CORS to arbitrary origins without a documented client need.
Recommended actions:
- Remove CORS from same-origin WebUI/local API routes unless a concrete supported cross-origin client requires it.
- If cross-origin access is required, restrict allowed origins to configured operator hosts and avoid credentialed wildcard or origin-mirroring behavior.
- Add origin or CSRF checks for state-changing browser routes.
- Set session cookies with
Securewhen served behind HTTPS andHttpOnlyunless browser JavaScript truly needs to read the cookie.
WebUI login lacks rate limiting
Paths:
src/rust/lqosd/src/node_manager/auth.rssrc/rust/lqos_config/src/authentication.rs
Short description:
The WebUI login path checks passwords with Argon2id for current hashes and
upgrades older SHA-256 hashes after successful login. The reviewed code did not
show per-IP, per-account, or global throttling for repeated failed login
attempts to the public /doLogin route.
Exposure / threat:
Under the Caddy setup, the login form is reachable through the control-plane
HTTPS entrypoint. An unauthenticated client can repeatedly submit passwords to
/doLogin; the server-side password hash is strong, but the reviewed code does
not throttle repeated failures before each verification attempt.
Recommended actions:
- Add rate limiting or exponential backoff for failed
/doLoginattempts, keyed by source address and username. - Log repeated failures in a way operators can act on without logging submitted passwords.
Observations / not findings
src/rust/lqos_setup/src/ssl.rsrenders a managed Caddyfile withadmin off, WebUI upstream127.0.0.1:9123, and API upstream127.0.0.1:9122.docs/v2.0/https-caddy.mddocuments moving the WebUI runtime listener to127.0.0.1:9123when HTTPS is enabled.../../lqos_api/src/web.rsmerges Swagger UI at/api-docs. This exposes endpoint shape, not credentials or control actions, and is not counted as a vulnerability by itself.allow_anonymousis an explicit read-only public/demo mode in the WebUI authentication configuration. It is not counted as a finding when the operator intentionally enables that mode.src/rust/lqos_setup/src/web.rsbinds the setup web service to0.0.0.0:9123and uses a setup token. Because this section assumes the runtime Caddy option is already installed, first-run setup exposure is left for a later setup/lifecycle audit unless a concrete bypass is found.
Bridged interface / eBPF malformed-traffic audit
Date: 2026-05-15
Scope assumptions:
- The two bridged interfaces are in scope for this section, including XDP, TC/eBPF, pinned maps, ring-buffer events, and userspace consumers of eBPF output.
- The control interface and Caddy/API/WebUI exposure are covered by the prior section and are out of scope here.
- Linux, Ubuntu, kernel, and NIC driver vulnerabilities are out of scope. This section reviews LibreQoS packet parser behavior, map pressure, debug logging, and userspace handling of eBPF output.
- No live XDP/TC attach-detach, pinned-map cleanup, or packet fuzzing was performed. Findings are based on static review of in-repo code.
Files and directories reviewed:
src/rust/lqos_sys/src/bpf/common/debug.hsrc/rust/lqos_sys/src/bpf/common/dissector.hsrc/rust/lqos_sys/src/bpf/common/dissector_tc.hsrc/rust/lqos_sys/src/bpf/common/flows.hsrc/rust/lqos_sys/src/bpf/common/heimdall.hsrc/rust/lqos_sys/src/bpf/common/lpm.hsrc/rust/lqos_sys/src/bpf/common/throughput.hsrc/rust/lqos_sys/src/bpf/common/maximums.hsrc/rust/lqos_sys/src/lqos_kernel.rssrc/rust/lqosd/src/throughput_tracker/flow_data/flow_analysis/kernel_ringbuffer.rssrc/rust/lqosd/src/throughput_tracker/tracking_data.rssrc/rust/lqos_heimdall/src/perf_interface.rssrc/rust/lqos_heimdall/src/timeline.rssrc/rust/lqos_heimdall/src/pcap.rs
Review searches:
rg "bpf_debug\\(|frag_off|ihl|tot_len|doff|BPF_MAP_TYPE_HASH|BPF_MAP_TYPE_PERCPU_HASH|BPF_MAP_TYPE_LRU|MAX_FLOWS|MAX_TRACKED_IPS|bpf_ringbuf_output|bpf_probe_read_kernel" src/rust/lqos_sys/src src/rust/lqosd/src/throughput_tracker src/rust/lqos_heimdall/src
Summary
- The XDP/TC packet dissectors use explicit
data_endbounds checks and bounded VLAN/MPLS loops. The review did not find memory-unsafe packet reads in the normal parser path. - Parser failures in
xdp_progreturnXDP_PASS; parser failures intc_iphash_to_cpureturnTC_ACT_OK. Unknown non-IP traffic therefore passes unshaped in the reviewed XDP/TC paths. - Five malformed-traffic / resource-exhaustion findings are listed below.
Packet-rate
bpf_trace_printkis reachable through thebpf_debugmacro from malformed short UDP/ICMP paths. - Several findings affect observability and flow analysis more than packet forwarding. They can still break LibreQoS operationally by hiding current flow/host state, filling pinned maps, or burning CPU on bridged-interface traffic.
Findings
Malformed packets can trigger packet-rate BPF trace logging
Paths:
src/rust/lqos_sys/src/bpf/common/debug.hsrc/rust/lqos_sys/src/bpf/common/dissector.hsrc/rust/lqos_sys/src/bpf/common/flows.hsrc/rust/lqos_sys/src/bpf/common/throughput.h
Short description:
bpf_debug(...) expands directly to bpf_trace_printk(...). Some call sites
are behind VERBOSE or TRACING, but several error paths reachable from packet
handling are not. Examples include truncated UDP/ICMP headers in
dissector.h and map insertion failures in flows.h and throughput.h.
Exposure / threat:
An attacker on a bridged interface can send malformed or high-cardinality
traffic that repeatedly hits these error paths. bpf_trace_printk is expensive
and writes into the kernel tracing path; at packet rate this can consume CPU and
trace-buffer bandwidth on the shaping host. Once map-pressure findings below
are triggered, failed insertions can also create a second packet-rate logging
path.
Recommended actions:
- Compile
bpf_debugto a no-op unlessVERBOSEorTRACINGis explicitly enabled. - Replace packet-rate error logging with counters in a bounded BPF map that userspace can poll at a low rate.
- Treat any remaining
bpf_trace_printkcall in XDP/TC packet paths as a debug build feature, not production behavior.
IPv4 fragments and invalid IPv4 header lengths can pollute flow tracking
Path:
src/rust/lqos_sys/src/bpf/common/dissector.h
Short description:
The XDP dissector verifies that an IPv4 header-sized region is present, then
uses iph->ihl * 4 to locate TCP, UDP, or ICMP headers. The reviewed code does
not reject ihl < 5, does not verify the IPv4 total length against the captured
packet bounds, and does not skip L4 snooping for fragmented IPv4 packets.
Exposure / threat:
Malformed IPv4 packets can make the dissector derive L4 ports and TCP flags from bytes that are not a valid L4 header. When those bytes make the apparent TCP data offset large enough, the TCP timestamp parser can also run against fragment payload rather than a real TCP options area. On the TCP path, this can seed or update Flowbee records, retransmit counters, and RTT sampling inputs with attacker-chosen fragment payload bytes.
UDP and ICMP fragments can also be interpreted as flow traffic if enough payload bytes are present. Those paths can create or update UDP/ICMP Flowbee entries from fragment payload instead of a valid UDP or ICMP header.
Recommended actions:
- Validate IPv4
version == 4,ihl >= 5, andl3offset + ihl * 4 <= data_endbefore any L4 header lookup. - Validate IPv4 total length enough to ensure the parsed L4 header is inside the IPv4 packet, not just inside the received frame.
- Skip L4 snooping and Flowbee updates for IPv4 fragments with non-zero fragment
offset or
MFset. Continue IP-level LPM shaping if desired. - Add a small packet corpus for malformed IPv4 IHL values, truncated TCP/UDP, and fragmented IPv4 packets.
UDP/ICMP spray can fill the non-LRU Flowbee map
Paths:
src/rust/lqos_sys/src/bpf/common/flows.hsrc/rust/lqos_sys/src/bpf/common/maximums.hsrc/rust/lqosd/src/throughput_tracker/tracking_data.rs
Short description:
flowbee is a pinned BPF_MAP_TYPE_HASH with MAX_FLOWS entries. The UDP and
ICMP handlers create a new Flowbee entry whenever no entry exists, even when the
IP mapping result has tc_handle == 0. TCP has a guard for non-SYN packets with
no mapping, but UDP and ICMP do not have the same shaped-traffic guard.
Exposure / threat:
Traffic with many spoofed IPs and ports can fill flowbee with unshaped UDP or
ICMP entries. When the map is full, later legitimate flow insertions fail and
LibreQoS loses current flow, RTT, retransmit, and QoE visibility for real
traffic. Each failed insert also reaches a bpf_debug path, which can amplify
the logging DoS above.
Recommended actions:
- Do not create UDP/ICMP Flowbee entries when
tc_handle == 0, unless a documented feature requires unshaped flow visibility. - Consider changing
flowbeeto an LRU map, or add a bounded admission policy for unshaped UDP/ICMP. - Expose Flowbee map pressure and insertion failures to userspace as counters, not trace logs.
- Add tests or a packet-replay harness that confirms unshaped UDP/ICMP sprays do not evict or block shaped TCP flow visibility.
Spoofed IP spray can fill the per-host traffic map
Paths:
src/rust/lqos_sys/src/bpf/common/throughput.hsrc/rust/lqos_sys/src/bpf/common/maximums.h
Short description:
map_traffic is declared as BPF_MAP_TYPE_PERCPU_HASH with
MAX_TRACKED_IPS = 128000. The comment says the map is LRU, but the declared
type is not an LRU map. track_traffic inserts a host counter for every parsed
IP host key, including unshaped traffic with tc_handle == 0.
Exposure / threat:
An attacker can send traffic with many spoofed source or destination IPs through
the bridge and fill the host counter map. Once full, new legitimate host
counters fail to insert, unknown-IP and per-host throughput visibility becomes
misleading, and every failed insertion can hit bpf_debug("Failed to insert flow").
Recommended actions:
- Change the map type to an LRU variant if eviction of old host counters is the intended behavior, or correct the comment and add explicit map-pressure handling.
- Avoid inserting unshaped hosts into
map_trafficunless operator-facing unknown-IP visibility requires it. - Add userspace counters for insert failures and map occupancy so operators can distinguish real quiet periods from map exhaustion.
Heimdall packet capture copies a fixed 128 bytes and ignores event-backpressure errors
Paths:
src/rust/lqos_sys/src/bpf/common/heimdall.hsrc/rust/lqos_heimdall/src/perf_interface.rssrc/rust/lqos_heimdall/src/pcap.rs
Short description:
When Heimdall analysis mode is enabled for a watched IP, the eBPF path copies
PACKET_OCTET_SIZE bytes from the packet start into each event and sends the
event through heimdall_events. The copy length is fixed at 128 bytes, the
return from bpf_probe_read_kernel is ignored, and the return from
bpf_ringbuf_output is ignored.
Exposure / threat:
This path is conditional on Heimdall watch mode, so it is not a default
bridging exposure. When enabled, short or malformed watched packets can produce
zero-padded or incomplete packet dumps without any signal to userspace. High
rate watched traffic can also fill the ring buffer. The current eBPF path
ignores the bpf_ringbuf_output return value, and the reviewed userspace path
has a collected-event counter and missed-tick warning but no surfaced
ring-buffer drop counter for Heimdall captures.
Recommended actions:
- Clamp the packet-copy length to the available packet length and
PACKET_OCTET_SIZE. - Check the return values from
bpf_probe_read_kernelandbpf_ringbuf_outputand increment bounded counters for copy failures and dropped events. - Surface Heimdall copy-failure and ring-buffer drop counters through the existing Heimdall/lqosd status path when packet capture is active.
Observations / not findings
- VLAN, PPPoE, and MPLS parsing uses bounded loops and
data_endchecks before walking encapsulation headers. This limits parser runtime on stacked headers. - Unknown non-IP traffic passes unshaped by design, preserving ARP, STP, IS-IS, and similar bridge traffic.
- IPv6 extension headers and IPv6 fragments are not deeply parsed for Flowbee L4 metrics. The reviewed path still performs IP-level LPM mapping for unsupported protocols, so this is an observability gap rather than a shaping bypass in the reviewed code.
- Flowbee RTT ring-buffer userspace handling validates event size before copying and uses a bounded in-process queue with coalesced wakeups.
- Runtime packet fuzzing, pinned-map occupancy checks, and live bridge-interface reachability were not performed in this stage.
Panic, error-handling, and type-loss audit
Date: 2026-05-15
Scope:
- Runtime Rust under
src/rust/, current Python entrypoints undersrc/, and sibling../../lqos_api/src/because the API is deployed behind Caddy. - Tests, generated output, vendored bindings, and historical Python copies were excluded unless a runtime path referenced them.
- This was a static source review. No live service, XDP/TC attach-detach, or packet replay was performed.
Review searches:
rg -n "\bpanic!\(|\.unwrap\(|\.expect\(|unreachable!\(|todo!\(|unimplemented!\(|assert!\(|from_raw_parts|transmute|unsafe \{|as (u8|u16|u32|usize|i8|i16|i32|f32)|unwrap_or_default\(|except Exception|except:|pass$" src/rust src --glob '*.py' ../../lqos_api/srcrg -n "as u32|as u16|as f32|partial_cmp\(.*\)\.unwrap|to_str\(\)\.unwrap|parse\(\)\.unwrap|try_into\(\)\.unwrap" src/rust ../../lqos_api/srcrg -n "except Exception|except:|pass$|sys.exit|int\(|float\(" src --glob '*.py' --glob '!LibreQoS-old.py' --glob '!LibreQoS-ancient.py' --glob '!LibreQoS.py.new'
Summary
- Four confirmed findings are listed below: one authenticated request-time panic, one request/websocket error-handling panic pattern, one queue-stat type-loss issue, and one NetFlow export type-loss issue.
- Two reachability-unknown hardening items are listed separately: API bandwidth float narrowing and RTT percentile sorting on floats.
- The malformed
x-bearerpanic at../../lqos_api/src/web_security.rs:32was already recorded in the network control-plane section and is not duplicated as a new finding here. - I did not find a confirmed memory-unsafe unsoundness issue in the reviewed
runtime paths. The high-volume unsafe hits were mostly FFI wrappers, generated
libbpf bindings, or callbacks that check event size before
from_raw_parts.
Findings
Authenticated packet-capture download can panic on missing capture file
Paths:
src/rust/lqosd/src/node_manager/local_api.rs:44src/rust/lqosd/src/node_manager/local_api/packet_analysis.rs:29src/rust/lqosd/src/node_manager/local_api/packet_analysis.rs:35
Short description:
The authenticated /local-api/pcapDump/:id route calls
n_second_pcap(id).expect(...) and later ServeFile::try_call(...).expect(...).
An invalid, expired, or removed packet-capture session can panic the request task
instead of returning a normal HTTP error.
Exposure / threat:
This route is behind the WebUI auth layer, so it is not an unauthenticated remote panic. A logged-in user, stale browser request, or automation using an old capture ID can still trigger request-time failure on the control plane. If the panic poisons shared state in the surrounding server path, the blast radius could be larger than one failed download.
Recommended actions:
- Return
404 Not Foundor410 Gonewhenn_second_pcap(id)cannot resolve a capture file. - Convert
ServeFile::try_callerrors into a bounded5xxresponse and log the underlying path/error once. - Add a focused route test for a missing capture ID and for a removed capture file.
Flow-explorer websocket handlers panic when time sources fail
Paths:
src/rust/lqosd/src/node_manager/local_api/flow_explorer.rs:42src/rust/lqosd/src/node_manager/local_api/flow_explorer.rs:45src/rust/lqosd/src/node_manager/local_api/flow_explorer.rs:104src/rust/lqosd/src/node_manager/local_api/flow_explorer.rs:107src/rust/lqosd/src/node_manager/local_api/flow_explorer.rs:117src/rust/lqosd/src/node_manager/local_api/flow_explorer.rs:120src/rust/lqosd/src/node_manager/ws.rs:665src/rust/lqosd/src/node_manager/ws.rs:674src/rust/lqosd/src/node_manager/ws.rs:683
Short description:
The flow-explorer timeline builders use expect(...) on time_since_boot() and
unix_now(). These functions are called from websocket message handlers for ASN,
country, and protocol timelines.
Exposure / threat:
The direct trigger is an operating-system time retrieval failure, not attacker controlled input. Still, once the condition exists, any authenticated websocket request for these timeline views can panic instead of returning an empty/error payload. This is incorrect request-time error handling on a control-plane feature.
Recommended actions:
- Return
Result<Vec<FlowTimeline>, Error>from the timeline builders and send a structured websocket error when the clock calls fail. - Reuse a small helper that computes boot time once with explicit logging.
- Add a focused unit test for the transport conversion path and a websocket test for the error response if the time helper is injectable.
Queue tracker silently narrows kernel qdisc counters
Paths:
src/rust/lqos_queue_tracker/src/queue_types/tc_cake.rs:102src/rust/lqos_queue_tracker/src/queue_types/tc_cake.rs:117src/rust/lqos_queue_tracker/src/queue_types/tc_cake.rs:187src/rust/lqos_queue_tracker/src/queue_types/tc_cake.rs:198src/rust/lqos_queue_tracker/src/queue_types/tc_cake.rs:206src/rust/lqos_queue_tracker/src/queue_types/tc_fq_codel.rs:61src/rust/lqos_queue_tracker/src/queue_types/tc_fq_codel.rs:62src/rust/lqos_queue_tracker/src/queue_types/tc_htb.rs:49src/rust/lqos_queue_tracker/src/queue_types/tc_htb.rs:50src/rust/lqos_queue_tracker/src/queue_types/tc_mq.rs:37src/rust/lqos_queue_tracker/src/queue_types/tc_mq.rs:38
Short description:
The qdisc JSON parsers read tc -s -j values as u64 and then cast many packet,
drop, backlog, flow, and memory counters to u32 or u16 with as. Rust's
integer narrowing casts wrap modulo the destination type, so large counters can
silently become smaller values.
Exposure / threat:
Busy shapers can exceed 32-bit packet/drop counters during normal operation. A traffic flood can make this happen faster. Wrapped queue stats can hide drops, mislead capacity and QoE views, and produce incorrect data for downstream operator or Insight decisions. This is data loss rather than memory corruption.
Recommended actions:
- Keep kernel counters as
u64through the queue tracker, bus messages, API serialization, and UI consumers unless the kernel field is truly bounded. - Where a protocol/UI contract must stay narrower, use
try_fromwith explicit clamp-and-warn behavior instead of uncheckedascasts. - Add qdisc parser fixtures with values above
u32::MAXandu16::MAX.
NetFlow 5 export can wrap flow counts, byte counts, and timestamps
Paths:
src/rust/lqosd/src/throughput_tracker/flow_data/netflow5/mod.rs:69src/rust/lqosd/src/throughput_tracker/flow_data/netflow5/protocol.rs:83src/rust/lqosd/src/throughput_tracker/flow_data/netflow5/protocol.rs:84src/rust/lqosd/src/throughput_tracker/flow_data/netflow5/protocol.rs:85src/rust/lqosd/src/throughput_tracker/flow_data/netflow5/protocol.rs:86src/rust/lqosd/src/throughput_tracker/flow_data/netflow5/protocol.rs:96src/rust/lqosd/src/throughput_tracker/flow_data/netflow5/protocol.rs:97src/rust/lqosd/src/throughput_tracker/flow_data/netflow5/protocol.rs:119src/rust/lqosd/src/throughput_tracker/flow_data/netflow5/protocol.rs:120
Short description:
The NetFlow 5 exporter narrows accumulator length, packet counters, byte counters,
and nanosecond timestamps to u16 or u32 with unchecked casts. One direction
converts timestamps to milliseconds before narrowing; the reverse record narrows
nanoseconds directly.
Exposure / threat:
NetFlow export is optional, but when enabled it can silently emit wrapped or inconsistent accounting for long-lived or high-throughput flows. External collectors may then undercount traffic, mis-order flow times, or make billing and abuse-analysis decisions from corrupted export records.
Recommended actions:
- Split NetFlow batches before the record count exceeds the protocol limit and
avoid unchecked
usize -> u16casts. - Convert timestamps consistently to the expected NetFlow units before narrowing.
- For NetFlow 5's inherently 32-bit fields, clamp with a warning or emit delta records before counters exceed protocol capacity.
- Prefer NetFlow 9/IPFIX-style export for counters that need wider fields.
Reachability unknown / hardening items
API site-speed changes narrow unbounded f64 request values to f32
Paths:
../../lqos_api/src/api/network_json.rs:126../../lqos_api/src/api/network_json.rs:127../../lqos_api/src/api/network_json.rs:128../../lqos_api/src/api/network_json.rs:129../../lqos_api/src/api/network_json.rs:230../../lqos_api/src/api/network_json.rs:234../../lqos_api/src/api/network_json.rs:237../../lqos_api/src/api/network_json.rs:241../../lqos_api/src/api/network_json.rs:711../../lqos_api/src/api/network_json.rs:714../../lqos_api/src/api/network_json.rs:717../../lqos_api/src/api/network_json.rs:720
Short description:
The API accepts site-speed values as Option<f64>, writes them into
network.json, and then narrows values to f32 for live bus commands and queue
mapping reads. The reviewed code did not show finite/range validation before the
narrowing casts.
Exposure / threat:
The route is bearer-authenticated, so this is not unauthenticated input. A credentialed caller can submit extremely large or nonsensical bandwidth values that round or become non-finite when narrowed, depending on downstream handling. The live command path may then diverge from the JSON value. I did not verify whether downstream bus receivers reject these values.
Recommended actions:
- Validate site-speed request values as finite, positive, and within explicit LibreQoS-supported Mbps bounds before writing JSON or sending live commands.
- Keep one numeric type across API, config, bus, and bakery code where practical.
- Add API tests for huge, negative, zero, fractional, and boundary bandwidth values.
API transit conversion can panic if RTT samples contain NaN
Path:
../../lqos_api/src/transit_types.rs:389
Short description:
NetworkJsonTransit::from sorts RTT samples with
partial_cmp(...).unwrap(). partial_cmp returns None for NaN, which makes the
conversion panic if a NaN reaches the RTT vector.
Exposure / threat:
The current in-repo RTT producers reviewed here mostly derive RTT values from
durations, which should be finite. I did not find a clear external input path to
inject NaN into this vector, so this is marked reachability unknown. If a NaN
does enter the telemetry state, several API endpoints that serialize
NetworkJsonTransit can panic while preparing a response.
Recommended actions:
- Filter non-finite RTT samples before sorting, or sort with
f32::total_cmp. - Add a small conversion test with
[10.0, f32::NAN]to prove the API response path does not panic.
Observations / not findings
../../lqos_api/src/web_security.rs:32remains a confirmed malformed-header panic, but it was already recorded in the network control-plane audit section.- Unsafe callback paths such as
src/rust/lqos_heimdall/src/perf_interface.rs:70throughsrc/rust/lqos_heimdall/src/perf_interface.rs:77check event size before creating a byte slice. This is not counted as a new unsoundness finding. - Broad Python exception handling exists in runtime files, including
src/LibreQoS.py:2361,src/LibreQoS.py:2364, andsrc/LibreQoS.py:2549. The sampled paths are shaping-input tolerance or planner-weight fallback behavior. They should be cleaned up for diagnosability, but I did not find a concrete security impact in this stage.
Node Manager privacy, auth, and XSS audit
Date: 2026-05-15
Scope:
- Node Manager HTTP routing, static page routing, auth/session handling,
websocket request handling, local API data providers, frontend source under
src/rust/lqosd/src/node_manager/js_build/src/, and the templated static HTML shell. - Generated bundles, vendored JavaScript, static images, and source maps were excluded from detailed XSS review unless a source file pointed to the behavior.
- This was a static source review. No browser exploit proof-of-concept was run.
Review searches:
rg -n "localStorage|sessionStorage|document\.cookie|innerHTML|outerHTML|insertAdjacentHTML|eval\(|Function\(|onclick=|onerror=|sanitize|DOMPurify|redact|redaction|redactable|allow_anonymous|auth_layer|route_layer|LoginResult|ReadOnly|Admin|Denied" src/rust/lqosd/src/node_manager docs/v2.0rg -n 'innerHTML\s*=.*(\+|)|simpleRowHtml(|href=.+|data-[^=]+=|textContent|innerText' src/rust/lqosd/src/node_manager/js_build/src --glob '.js'`rg -n "ShapedDevice|network_json|CircuitById|AllShapedDevices|NetworkJson|Search|UnknownIps|CircuitDirectory|device_name|circuit_name|mac|ipv4|ipv6|comment" src/rust/lqosd/src/node_manager
Summary
- Four confirmed findings are listed below: raw PII exposure in anonymous/read-only surfaces, multiple DOM XSS sinks for operator/customer fields, JS-readable session cookies, and dashboard-theme writes/XSS reachable by read-only users.
- I did not find a websocket path that serves data before the handshake completes:
the server sends
Hello, requiresHelloReply, rejects denied tokens, and closes on other pre-handshake messages. - Static HTML pages and
/local-api/*are routed throughauth_layer. The fallback static file server remains a hardening concern, but the reviewed currentstatic2files are static assets or pages already listed in the authenticated router. - No session token or API key was found in
localStorage. The higher-risk browser storage issue is that the session cookie is readable by JavaScript so the websocket client can copy it into the handshake.
Findings
Anonymous/read-only mode exposes raw subscriber and topology data without server-side anonymization
Paths:
src/rust/lqos_config/src/authentication.rs:487src/rust/lqos_config/src/authentication.rs:498src/rust/lqosd/src/node_manager/auth.rs:421src/rust/lqosd/src/node_manager/auth.rs:431src/rust/lqosd/src/node_manager/ws.rs:2199src/rust/lqosd/src/node_manager/ws.rs:2207src/rust/lqosd/src/node_manager/local_api/config.rs:528src/rust/lqosd/src/node_manager/local_api/config.rs:541src/rust/lqosd/src/node_manager/local_api/shaped_devices_page.rs:118src/rust/lqosd/src/node_manager/local_api/shaped_devices_page.rs:123src/rust/lqosd/src/node_manager/local_api/shaped_devices_page.rs:131docs/v2.0/components.md:123docs/v2.0/components.md:126src/rust/lqosd/src/node_manager/js_build/src/helpers/redact.js:48
Short description:
The documented anonymous option grants unauthenticated users ReadOnly access.
Read-only websocket requests include raw network.json and all shaped devices.
Those payloads include circuit names/IDs, device names/IDs, parent node names,
MACs, comments, and IPv4/IPv6 addresses. The current redaction feature is a
client-side display blur/font mode, documented as not modifying source data.
Exposure / threat:
Anonymous read-only mode is intended for demos, but demos are exactly where PII redaction matters. Anyone who can reach a node with anonymous mode enabled can request raw payloads directly over the websocket, regardless of whether the UI redaction toggle is on. Read-only authenticated users also receive the same raw data, so redaction is not a privacy boundary.
Recommended actions:
- Add a server-side anonymized/demo payload mode for anonymous access. Replace subscriber/circuit/device names, IDs, IPs, MACs, and comments before sending websocket responses.
- Consider field-level filtering for normal
ReadOnlyusers where full subscriber/device identifiers are not needed. - Keep browser redaction as a screenshot tool, but do not rely on it for public demo privacy.
- Add tests that anonymous mode cannot retrieve raw
AllShapedDevices,NetworkJson, circuit search, or circuit detail identifiers.
Operator-controlled names reach innerHTML without escaping
Paths:
src/rust/lqosd/src/node_manager/js_build/src/template.js:797src/rust/lqosd/src/node_manager/js_build/src/template.js:799src/rust/lqosd/src/node_manager/js_build/src/template.js:801src/rust/lqosd/src/node_manager/js_build/src/circuit.js:2164src/rust/lqosd/src/node_manager/js_build/src/dashlets/top10_downloaders.js:97src/rust/lqosd/src/node_manager/js_build/src/dashlets/worst10_downloaders.js:97src/rust/lqosd/src/node_manager/js_build/src/dashlets/worst10_retransmits.js:101src/rust/lqosd/src/node_manager/js_build/src/config_users.js:101src/rust/lqosd/src/node_manager/js_build/src/config_users.js:104src/rust/lqosd/src/node_manager/js_build/src/helpers/builders.js:45
Short description:
Several frontend paths concatenate circuit names, device names, site names,
dashboard/user fields, or other operator-controlled values into innerHTML. Some
nearby code has local escapeHtml helpers, but these sinks do not consistently
use them. simpleRowHtml is a reusable helper that writes its argument directly
to td.innerHTML.
Exposure / threat:
Circuit/device/site names can come from operator files or external integrations. A malicious or compromised integration record containing HTML can execute script when a user searches, opens a circuit page, views dashboard top lists, or opens the user management page. Because the WebUI controls shaping and configuration, stored XSS should be treated as a control-plane compromise path, not a cosmetic bug.
Recommended actions:
- Replace these sinks with DOM construction and
textContentfor untrusted text. Keep icons as separately created<i>elements. - Ban
simpleRowHtmlfor untrusted data. UsesimpleRowor an escaped helper with a name that makes trust explicit. - Escape attribute values separately from text nodes and validate URL protocols before assigning links.
- Add frontend tests or a small DOM fixture using a circuit name like
<img src=x onerror=alert(1)>to confirm it renders as text everywhere.
WebUI session token is readable by JavaScript and copied into websocket auth
Paths:
src/rust/lqosd/src/node_manager/auth.rs:266src/rust/lqosd/src/node_manager/auth.rs:269src/rust/lqosd/src/node_manager/js_build/src/pubsub/ws.js:66src/rust/lqosd/src/node_manager/js_build/src/pubsub/ws.js:78
Short description:
The User-Token session cookie is created with SameSite=Lax, but it is not
marked HttpOnly or Secure. The frontend websocket client reads
document.cookie and sends the session token in the websocket handshake.
Exposure / threat:
Any XSS in Node Manager can read and exfiltrate the signed session token. That
turns DOM injection into session theft, and an admin session can perform
configuration and shaping actions. Secure should also be set when the Caddy/TLS
option is active so the browser will not send the cookie over plain HTTP.
Recommended actions:
- Move websocket authentication to the server-side
Cookieheader during the upgrade, or issue a short-lived websocket nonce that is not the durable session token. - Set
HttpOnlyon the session cookie once the websocket no longer needs JavaScript cookie access. - Set
Securewhenever Node Manager is served through HTTPS/Caddy, with a documented local-development exception if needed. - Add an XSS regression test that verifies the session token is not available via
document.cookie.
Read-only websocket users can write dashboard themes, enabling stored XSS
Paths:
src/rust/lqosd/src/node_manager/ws.rs:354src/rust/lqosd/src/node_manager/ws.rs:371src/rust/lqosd/src/node_manager/ws.rs:378src/rust/lqosd/src/node_manager/local_api/dashboard_themes.rs:35src/rust/lqosd/src/node_manager/local_api/dashboard_themes.rs:60src/rust/lqosd/src/node_manager/local_api/dashboard_themes.rs:97src/rust/lqosd/src/node_manager/js_build/src/lq_js_common/dashboard/dashboard.js:1024
Short description:
The websocket dashboard-theme save/get/delete handlers do not check for
LoginResult::Admin. dashboard_themes only normalizes / and \ in the theme
filename, stores the provided name, and later returns it for display. The saved
layout list writes d.name into innerHTML.
Exposure / threat:
Any authenticated read-only user, and any anonymous user when demo mode is enabled, can save a dashboard layout with a malicious name. When an admin or operator opens the saved-layout picker, that name can execute as stored XSS and steal the JS-readable session token described above.
Recommended actions:
- Require
LoginResult::AdminforDashletSaveandDashletDelete, or document and constrain why read-only users should be allowed to write shared layouts. - Escape
d.namewhen rendering saved layouts, preferably withtextContent. - Validate dashboard theme names server-side with an allowlist of printable characters and a length limit.
- Add websocket tests for read-only denial and an XSS fixture for saved layout names.
Hardening observations / not findings
src/rust/lqosd/src/node_manager/ws.rs:311throughsrc/rust/lqosd/src/node_manager/ws.rs:329require the websocketHelloReplybefore processing normal requests. This review did not find a pre-handshake data response.src/rust/lqosd/src/node_manager/static_pages.rs:114throughsrc/rust/lqosd/src/node_manager/static_pages.rs:117apply auth and templates to the listed HTML pages, andsrc/rust/lqosd/src/node_manager/local_api.rs:66appliesauth_layerto/local-api.src/rust/lqosd/src/node_manager/run.rs:85still has an unauthenticated fallbackServeDir; keep it restricted to static assets and avoid adding data files or unlisted HTML pages understatic2.src/rust/lqosd/src/node_manager/local_api/config.rs:341throughsrc/rust/lqosd/src/node_manager/local_api/config.rs:349require admin access and redact integration secrets before returning the main config view.src/rust/lqosd/src/node_manager/js_build/src/config_interface.js:4,src/rust/lqosd/src/node_manager/js_build/src/config_interface.js:5,src/rust/lqosd/src/node_manager/js_build/src/config_interface.js:376, andsrc/rust/lqosd/src/node_manager/js_build/src/config_interface.js:424store network-mode drafts and pending operation IDs inlocalStorage. I did not find credentials there, but interface names, VLANs, and operation IDs are operationally sensitive on shared browsers. PrefersessionStorageor clear these keys on logout, confirm, and rollback.
Conclusion
Overall security grade: C.
LibreQoS is not starting from a bad place. The review found no confirmed
reachable CVE-class Rust dependency issue, the managed Caddy setup moves the
intended WebUI path toward loopback-backed TLS, Node Manager has a real
authentication/role model, eBPF packet parsing uses many explicit data_end
checks and bounded encapsulation loops, and several sensitive admin config values
are already redacted before they reach the browser. Those are meaningful
strengths, and they reduce the chance that the next release ships with a simple
dependency or parser-memory-safety failure.
The weaknesses are concentrated around control-plane hardening and browser trust boundaries. The biggest current risks are WebUI stored/DOM XSS, a JavaScript readable session token, raw PII exposure in anonymous/read-only demo paths, an API direct-listener path that can bypass the intended Caddy/TLS route if exposed, request-time panics on malformed or stale inputs, and packet-path behavior that can lose observability or consume CPU under malformed/high-cardinality bridged traffic.
The release posture should be: acceptable foundations, but not yet polished security hygiene. Fixing the Node Manager XSS/session/anonymization issues and the API/control-plane panic/listener issues would move the project materially toward a B. Addressing the eBPF map-pressure/debug-logging findings, adding rate limiting, and cleaning up numeric type-loss paths would make the security state much more comfortable for routine release cadence.