Skip to content

Fix 10 security and correctness bugs (auth bypass, SQL injection, TOCTOU, panics, negative uptime)#21

Merged
AlexanderWagnerDev merged 5 commits into
mainfrom
claude/error-bug-review-d8fq84
Jun 30, 2026
Merged

Fix 10 security and correctness bugs (auth bypass, SQL injection, TOCTOU, panics, negative uptime)#21
AlexanderWagnerDev merged 5 commits into
mainfrom
claude/error-bug-review-d8fq84

Conversation

@AlexanderWagnerDev

@AlexanderWagnerDev AlexanderWagnerDev commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Security and correctness review of librtmp2-server. Ten confirmed bugs fixed across db.rs, http.rs, keygen.rs, rtmp_bridge.rs, and server.rs.

Security fixes

  • Auth bypass (http.rs bearer_ok): When api_token was empty the function returned true, making the entire REST API publicly accessible without a token. Changed to return false — an unconfigured token denies all access, forcing operators to set a real secret.

  • SQL injection (db.rs stream_find_by): The column parameter was interpolated directly into the SQL string via format!. Although all three call sites pass literal column names today, the function was private with no guard against future misuse. Added an explicit allowlist ("publish_key" | "play_key" | "stats_key") that returns None and logs an error for any other value.

  • World-writable default DB path (server.rs): The fallback database path was /tmp/librtmp2-server.db, a world-writable directory where a local attacker could plant a symlink before first launch. Changed to ./librtmp2-server.db.

Correctness fixes

  • TOCTOU on stream delete (http.rs handle_stream_delete): Existence was checked via stream_get() (lock Update to Mongoose 7.14 API and fix build configuration #1), then the record was deleted via stream_delete() (lock fix: build fixes for server, add docs #2). A concurrent delete between those two acquisitions could succeed silently. Fixed by extending stream_delete() to return Option<bool> (Some(true) = deleted, Some(false) = not found, None = DB error) so the handler makes a single atomic decision.

  • Negative uptime (http.rs build_json_stats / build_nginx_xml): now - p.connected_at could be negative if the system clock moved backward or connected_at was stored slightly in the future. Added .max(0) on all four uptime calculations.

  • gen_id collision risk (rtmp_bridge.rs): IDs were {timestamp}_{32-bit random}, so two connections arriving in the same second shared the upper bits and had only 2³² possible IDs to avoid each other. Replaced with a single 64-bit random value ({:016x}) — no timestamp, 2⁶⁴ space.

  • Untracked on_close (rtmp_bridge.rs): When on_close fired for a ConnId not in the map (e.g. on_connect was never called, or the connection was already cleaned up), it silently returned. Added a log_warn! so ghost-connection scenarios are visible in logs.

Panic prevention

  • now_ts() panics before UNIX_EPOCH (db.rs, http.rs): duration_since(UNIX_EPOCH).unwrap() panics if the system clock is before 1970. Changed to .unwrap_or_default() (returns 0).

  • keygen_secret panics on RNG failure (keygen.rs): .expect("OS RNG failure") would panic inside an async handler, crashing the Tokio worker. Changed the return type to Result<String, String>; callers in handle_stream_create now return HTTP 500 on failure.

  • .unwrap() on DB prepare() / query_map() (db.rs stream_list, publisher_list, player_list, stat_recent): A panic on prepare/query would poison the Mutex<Connection>, making the database permanently inaccessible for the lifetime of the process. Replaced with let Ok(mut stmt) = … else { return Vec::new(); } and .unwrap_or_default().

Test plan

  • All 19 existing unit tests pass (cargo test)
  • stream_delete_cascades_active_publishers updated to assert!(matches!(…, Some(true)))
  • keygen::tests::keys_are_unique_and_well_formed updated to unwrap Result
  • No new test infrastructure required — the changes are defensive paths

Generated by Claude Code

Summary by CodeRabbit

  • Bug Fixes / Security

    • Prevented crashes when system time is out of range; key generation now fails gracefully instead of panicking.
    • Tightened bearer auth by denying requests when the API token is empty.
    • Fixed stats uptime to never report negative durations.
    • Hardened stream endpoints: only allowed query columns, safer listings/stats when queries fail, and stream deletion now returns clearer status outcomes.
    • Restricted database file permissions to 0600 (and sidecars) on Unix.
  • Maintenance / Tests

    • Safer RTMP disconnect handling and updated stream ID format.
    • Startup now requires LRTMP2_DB.
    • Updated tests for auth and cascade-delete behavior.

- db: now_ts() uses unwrap_or_default() instead of panicking on pre-epoch clock
- db: stream_find_by() validates column name against allowlist to prevent SQL injection
- db: stream_delete() returns Option<bool> (Some(true)=deleted, Some(false)=not found,
  None=error) so callers can distinguish not-found from failure
- db: stream_list/publisher_list/player_list/stat_recent use let-else on prepare() and
  .unwrap_or_default() on query_map() instead of panicking, preventing mutex poisoning
- http: now_ts() uses unwrap_or_default() for same reason as db
- http: bearer_ok() returns false (not true) when api_token is empty, closing auth bypass
- http: uptime calculations clamp to .max(0) to prevent negative values when connected_at
  is slightly in the future due to clock skew
- http: handle_stream_delete uses single stream_delete() call, eliminating TOCTOU race
  between stream_get() and stream_delete() across two separate mutex acquisitions
- keygen: keygen_secret returns Result<String,String> instead of panicking on RNG failure
- rtmp_bridge: gen_id uses 64-bit random alone (drops timestamp) to eliminate collision
  risk under concurrent connections sharing the same second
- rtmp_bridge: on_close logs a warning for untracked connections instead of silently
  returning and leaking ghost rows
- server: default DB path changed from /tmp (world-writable, symlink attack) to ./

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjjkQ1isQqDyBxn6E8eFWq
Copilot AI review requested due to automatic review settings June 30, 2026 05:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2d15073c-078f-4394-bd05-6bbb11ccee31

📥 Commits

Reviewing files that changed from the base of the PR and between 72dfc9a and 309f3b0.

📒 Files selected for processing (1)
  • src/db.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/db.rs

📝 Walkthrough

Walkthrough

Replaces panics and unwraps with fallible handling across key generation, database access, HTTP stream creation/deletion, and runtime helpers. It also tightens bearer auth, clamps uptime values, changes stream ID formatting, and requires an explicit SQLite database path.

Changes

Error Hardening and Runtime Behavior

Layer / File(s) Summary
Fallible key generation
src/keygen.rs, src/http.rs
keygen_secret now returns Result<String, String>, tests unwrap the generated values, and stream creation handles key-generation failures explicitly.
HTTP auth, stats, and delete responses
src/http.rs
now_ts uses unwrap_or_default. Empty API tokens now fail bearer auth. JSON and XML uptime values are clamped to zero. Stream deletion matches Option<bool> from the database. A test covers empty-token denial.
DB open, guards, and delete flow
src/db.rs
now_ts uses unwrap_or_default. Db::open() restricts SQLite file permissions on Unix. stream_find_by enforces a column allowlist. stream_delete returns Option<bool> with transactional cascade handling. stream_list returns empty vectors on prepare/query failures. The cascade-delete test expects Some(true).
DB publisher, player, and recent stat queries
src/db.rs
publisher_list, player_list, and stat_recent guard statement preparation and default to empty results on failures.
ID format and startup checks
src/rtmp_bridge.rs, src/server.rs
gen_id now uses a random u64 formatted as 16 hex digits. on_close logs missing connection state. ServerApp::create now errors when LRTMP2_DB is unset or empty.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Poem

🐇 I hopped through keys that now can fail,
and database paths that no longer trail.
No unwrap thumps, no token sigh,
just tidy hops as errors fly.
With 16 hex and uptimes set right,
the burrow hums through day and night.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main security and correctness fixes, including auth bypass, SQL injection, TOCTOU, and panic/uptime issues.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/error-bug-review-d8fq84

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/http.rs (2)

116-119: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add an empty-token auth regression test.

This is the bypass being fixed; the visible test covers a missing header with a non-empty token, but not api_token == "". A request with any/missing bearer token should still return 401.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/http.rs` around lines 116 - 119, The auth check in bearer_ok should be
covered with a regression test for the empty api_token bypass: add a test case
around the bearer auth path in src/http.rs that sets AppState.config.api_token
to an empty string and verifies requests with any bearer token and with no
Authorization header both return 401. Reuse the existing bearer_ok behavior and
the HTTP request handling/tests around it so the new test clearly exercises the
empty-token branch.

418-430: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Log key generation failures without exposing secrets.

The _ branch returns 500 but drops the RNG error, leaving operators no diagnostic. Capture each Err(e) individually and log only the error/prefix context, not any successfully generated keys.

Example refactor
-    let (publish_key, play_key, stats_key) = match (
-        keygen_secret("pub_"),
-        keygen_secret("pl_"),
-        keygen_secret("st_"),
-    ) {
-        (Ok(a), Ok(b), Ok(c)) => (a, b, c),
-        _ => {
-            return err_json(
-                StatusCode::INTERNAL_SERVER_ERROR,
-                "INTERNAL_ERROR",
-                "Key generation failed",
-            )
-        }
-    };
+    let publish_key = match keygen_secret("pub_") {
+        Ok(key) => key,
+        Err(e) => {
+            crate::log_error!("Publish key generation failed: {e}");
+            return err_json(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", "Key generation failed");
+        }
+    };

Repeat for play_key and stats_key.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/http.rs` around lines 418 - 430, The key generation path in the match
over keygen_secret("pub_"), keygen_secret("pl_"), and keygen_secret("st_")
currently hides the underlying RNG failure, so update this branch to capture
each Err(e) separately and log the failure with only the prefix/error context.
Keep the existing err_json response from the surrounding logic, but use the
local symbols publish_key, play_key, and stats_key handling to ensure no
successfully generated key values are ever logged or exposed. Repeat the same
error logging pattern for the play_key and stats_key branches so operators get
diagnostics without secrets.
src/db.rs (1)

282-288: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Log DB failures before returning hardened fallbacks.

These paths now avoid panics, but commit/prepare/query/row-mapping errors are silently collapsed to None or Vec::new(). That can make a DB outage or schema mismatch look like “no streams/clients” in stats.

Example pattern
-                if tx.commit().is_ok() {
-                    Some(rows > 0)
-                } else {
-                    None
-                }
+                match tx.commit() {
+                    Ok(()) => Some(rows > 0),
+                    Err(e) => {
+                        crate::log_error!("DB cascade delete commit error for {id}: {e}");
+                        None
+                    }
+                }

Apply the same pattern to prepare, query_map, and row conversion failures before returning empty vectors.

Also applies to: 298-308, 396-415, 507-526, 562-582

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/db.rs` around lines 282 - 288, Log database failures before returning
fallback values in the affected query paths: in the logic around the existing
result handling, plus the `prepare`, `query_map`, and row-conversion code paths,
capture and report the actual error before collapsing to `None` or `Vec::new()`.
Use the existing DB helper methods and match arms in `src/db.rs` as the
insertion points so commit/prepare/query/row-mapping failures are visible in
logs while still returning hardened fallbacks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/server.rs`:
- Around line 26-29: The database path fallback in the server startup logic is
cwd-relative, which makes persistence depend on the working directory. Update
the db path handling in the server initialization code around the existing
LRTMP2_DB lookup so that it either requires LRTMP2_DB to be set and fails fast
when missing, or resolves to a stable absolute data directory instead of using
"./librtmp2-server.db". Keep the change localized to the db_path setup in
src/server.rs so the rest of the server and database initialization paths
continue to use the resolved path unchanged.

---

Nitpick comments:
In `@src/db.rs`:
- Around line 282-288: Log database failures before returning fallback values in
the affected query paths: in the logic around the existing result handling, plus
the `prepare`, `query_map`, and row-conversion code paths, capture and report
the actual error before collapsing to `None` or `Vec::new()`. Use the existing
DB helper methods and match arms in `src/db.rs` as the insertion points so
commit/prepare/query/row-mapping failures are visible in logs while still
returning hardened fallbacks.

In `@src/http.rs`:
- Around line 116-119: The auth check in bearer_ok should be covered with a
regression test for the empty api_token bypass: add a test case around the
bearer auth path in src/http.rs that sets AppState.config.api_token to an empty
string and verifies requests with any bearer token and with no Authorization
header both return 401. Reuse the existing bearer_ok behavior and the HTTP
request handling/tests around it so the new test clearly exercises the
empty-token branch.
- Around line 418-430: The key generation path in the match over
keygen_secret("pub_"), keygen_secret("pl_"), and keygen_secret("st_") currently
hides the underlying RNG failure, so update this branch to capture each Err(e)
separately and log the failure with only the prefix/error context. Keep the
existing err_json response from the surrounding logic, but use the local symbols
publish_key, play_key, and stats_key handling to ensure no successfully
generated key values are ever logged or exposed. Repeat the same error logging
pattern for the play_key and stats_key branches so operators get diagnostics
without secrets.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 294180e1-3dfb-4012-88be-aa9923c703e3

📥 Commits

Reviewing files that changed from the base of the PR and between ea20b5d and e82f28e.

📒 Files selected for processing (5)
  • src/db.rs
  • src/http.rs
  • src/keygen.rs
  • src/rtmp_bridge.rs
  • src/server.rs

Comment thread src/server.rs Outdated
claude added 3 commits June 30, 2026 05:43
- server: fail fast with a clear error when LRTMP2_DB is unset instead of
  falling back to a cwd-relative path that silently opens a different database
  each time the process is started from a different directory
- http: log each keygen failure individually (publish/play/stats prefix + error)
  before returning 500, so operators have diagnostics without any key material
  being logged
- http: add empty_api_token_always_denies test covering the api_token=="" bypass
  both with no Authorization header and with a bearer token — both must 401
- db: log before returning silent fallbacks; stream_delete commit failure, and
  prepare() failures in stream_list/publisher_list/player_list/stat_recent all
  now emit log_error! so a DB outage is visible in logs rather than appearing
  as empty results

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjjkQ1isQqDyBxn6E8eFWq
Stream keys (publish_key, play_key, stats_key) stored in SQLite are
sensitive; on multi-user hosts the default umask leaves the DB file
world-readable (0644). Added restrict_db_file_permissions() which sets
the DB file and its WAL/SHM siblings to owner-only (0600) immediately
after Connection::open(). Added a test to verify the mode.

Addresses the fix described in PR #20 applied on top of the latest main
(which already has the bearer_ok empty-token fix from our PR #21).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjjkQ1isQqDyBxn6E8eFWq

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/db.rs`:
- Around line 145-161: `restrict_db_file_permissions` is trying to chmod every
database path, including special non-file values like `:memory:` that `Db::open`
can pass in during tests and runtime. Update `restrict_db_file_permissions` to
detect and skip non-files/URIs/empty paths before calling `fs::set_permissions`,
so `log_warn!` only runs for real on-disk databases when the main candidate
actually exists. Keep the existing WAL/SHM handling for valid file-backed paths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a039162e-208a-4e9e-964f-55a418f2497b

📥 Commits

Reviewing files that changed from the base of the PR and between dea8b20 and 72dfc9a.

📒 Files selected for processing (1)
  • src/db.rs

Comment thread src/db.rs
:memory: and file: URI paths have no backing file. Calling
fs::set_permissions on them fails with NotFound and incorrectly
triggered log_warn! during every unit test open, masking genuine
permission failures on real database files.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjjkQ1isQqDyBxn6E8eFWq
@AlexanderWagnerDev AlexanderWagnerDev merged commit 778acfd into main Jun 30, 2026
2 checks passed
@AlexanderWagnerDev AlexanderWagnerDev deleted the claude/error-bug-review-d8fq84 branch June 30, 2026 12:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants