Skip to content

Harden server auth, deletes, and rate limiting#56

Merged
AlexanderWagnerDev merged 9 commits into
mainfrom
cursor/security-review-fixes
Jul 7, 2026
Merged

Harden server auth, deletes, and rate limiting#56
AlexanderWagnerDev merged 9 commits into
mainfrom
cursor/security-review-fixes

Conversation

@AlexanderWagnerDev

@AlexanderWagnerDev AlexanderWagnerDev commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Seed API token from LRTMP2_API_TOKEN on first startup and mask generated tokens in logs
  • Add RTMP auth failure rate limiting, async stream delete (202), and safer viewer revocation
  • Restrict public /health and stats responses; improve HTTP rate-limit LRU eviction and DB error handling

Test plan

  • cargo test in librtmp2-server
  • Verify Docker startup with shared LRTMP2_API_TOKEN
  • Exercise stream delete with active RTMP sessions (expect 202 then background cleanup)

View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.

Summary by CodeRabbit

  • New Features
    • /api/v1/health now returns a minimal unauthenticated response, and a richer Bearer-auth response with timestamps and RTMP/RTMPS port details.
    • Public /stats endpoints now include consistent response pacing and improved stats key handling.
  • Bug Fixes
    • Stream deletion is non-blocking when active RTMP connections exist (returns 202 with {status:"deleting"} and finalizes in the background).
    • IP-based RTMP publish/play auth throttling now persists across reconnects and expires via a sliding window.
    • Rate limiting evicts the oldest tracked client bucket when at capacity.
  • Documentation / Tests
    • Updated .env.example and clarified LRTMP2_API_TOKEN seeding/persistence behavior; health/auth E2E tests updated accordingly.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1f071ceb-8283-484a-a864-6eb41f2b6ed2

📥 Commits

Reviewing files that changed from the base of the PR and between d4ede1c and 9ae2aa0.

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

📝 Walkthrough

Walkthrough

This PR updates token seeding and documentation, changes health/stats/deletion HTTP behavior, adds stream-delete recovery state, alters rate-limiter eviction, adds RTMP auth-failure limiting, and changes publisher count-query failure handling.

Changes

Server behavior updates

Layer / File(s) Summary
Token seeding and bootstrap
src/server.rs, src/config.rs, .env.example
API token guidance and bootstrap token resolution now describe DB-backed storage, env seeding, token validation, and pending stream-delete recovery.
Stream delete persistence
src/db.rs
The streams table gains pending_delete, startup migration adds it for older databases, disable/enable updates the flag, and pending-delete stream IDs can be queried.
Publisher count query handling
src/db.rs
The active-publisher count query now logs and fails closed on query error instead of defaulting to one active publisher.
Health, stats, and deletion flow
src/http.rs, tests/rtmp_http_e2e.rs
/api/v1/health returns minimal or detailed JSON by auth, stats handlers use keyed lookup with pacing, stream deletion can return 202 deleting, and tests cover the new health responses.
Rate limiter capacity eviction
src/rate_limit.rs
When the tracking map is full, the oldest tracked client bucket is evicted and the new client is admitted; the unit test reflects that behavior.
RTMP auth failure limiting
src/rtmp_bridge.rs, src/server.rs
RTMP publish/play authorization now tracks recent failures per remote IP, rate-limits repeated failures, and uses safer bridge access plus updated connection wiring.

Estimated code review effort: 5 (Critical) | ~90 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and captures the main themes of the PR: auth hardening, asynchronous deletes, and rate limiting.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 cursor/security-review-fixes

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

@AlexanderWagnerDev AlexanderWagnerDev marked this pull request as ready for review July 7, 2026 14:45

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 595a97e26a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/rtmp_bridge.rs Outdated
result
}

fn try_authorize_publish(&self, conn: ConnId, app: &str, stream_key: &str) -> Result<(), ()> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Move helper out of the trait impl

This helper is declared inside impl RtmpEventHandler for DbRtmpBridge, but RtmpEventHandler only defines on_connect, authorize_publish, authorize_play, on_frame, and on_close. Rust rejects extra methods in a trait impl, so this module will not compile when the crate is built; make this an inherent impl DbRtmpBridge helper (and do the same for try_authorize_play) or inline it.

Useful? React with 👍 / 👎.

Comment thread src/rate_limit.rs Outdated
fn evict_oldest_client(&self, guard: &mut HashMap<String, Vec<Instant>>) {
let Some(oldest_key) = guard
.iter()
.min_by_key(|(_, entries)| entries.first().copied().unwrap_or(Instant::now()))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use last-seen time for LRU eviction

When MAX_TRACKED_KEYS is full, this chooses the bucket with the oldest first request, not the least-recently-seen bucket. Because buckets are stored oldest-to-newest, a client with recent traffic but one older in-window entry can be evicted and have its counter reset when an attacker fills the client map, weakening the per-IP/path rate limit; use the newest retained timestamp instead.

Useful? React with 👍 / 👎.

Comment thread src/server.rs Outdated
{}\n\
Set LRTMP2_API_TOKEN in the panel .env to this value.\n\
============================================================",
mask_api_token(&candidate)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Print the generated token before requiring it

When LRTMP2_API_TOKEN is not provided, this masks the freshly generated API token before printing it. That token is immediately required for every admin endpoint and there is no unauthenticated retrieval path, so a first-time operator using the default startup path cannot configure the panel or make API calls without manually opening SQLite; keep the one-time generated secret visible or provide another retrieval mechanism.

Useful? React with 👍 / 👎.

Comment thread .env.example

# Optional shared API bearer token (recommended for Docker). When set, the server
# stores this value in SQLite on first startup instead of generating a new one.
# LRTMP2_API_TOKEN=<generate-with-openssl-rand-hex-32>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Load the advertised token from the config file

Uncommenting this line in the copied .env will not seed the database: the config loader applies only the unprefixed server keys and resolve_api_token reads std::env::var("LRTMP2_API_TOKEN") instead of values parsed from .env. In the normal librtmp2-server startup that parses .env itself, the server will generate a different token and a panel configured with this value will receive 401s.

Useful? React with 👍 / 👎.

Comment thread src/rtmp_bridge.rs Outdated
db: Arc<Db>,
conns: Mutex<HashMap<ConnId, ConnState>>,
deleted_streams: Arc<Mutex<HashSet<String>>>,
auth_failures: Mutex<HashMap<ConnId, Vec<Instant>>>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Key RTMP auth throttling by peer, not connection

The new failure bucket is keyed only by ConnId, and on_close clears that bucket, so a client can avoid the RTMP auth limit by reconnecting between failed publish/play attempts. Since failed auth attempts are tied to short-lived RTMP connections, this does not slow brute-force attempts across sessions; use a stable peer/key scope if this is meant to protect RTMP credentials.

Useful? React with 👍 / 👎.

Comment thread src/server.rs Outdated
Comment on lines +733 to +735
std::env::set_var("LRTMP2_API_TOKEN", env_token);
let app = ServerApp::bootstrap(ServerConfig::default(), db_path_str).expect("bootstrap");
std::env::remove_var("LRTMP2_API_TOKEN");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Wrap env mutation in unsafe blocks

This crate is edition 2024, where std::env::set_var and std::env::remove_var are unsafe APIs. This new test uses both as safe calls, so cargo test will fail to compile this target until the environment mutation is wrapped appropriately, ideally with serialization around the global process environment.

Useful? React with 👍 / 👎.

@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: 6

🧹 Nitpick comments (3)
src/db.rs (1)

828-834: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider applying the same explicit error logging to player_try_acquire's COUNT query for consistency.

This query still relies on a silent unwrap_or fallback (falling back to the cap so it fails closed), unlike the newly-hardened publisher_try_acquire above, which now logs and returns false explicitly on query failure. Not a correctness issue since both fail closed, but the inconsistency in error observability between the two acquire paths could be worth aligning.

🤖 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 828 - 834, The COUNT query in player_try_acquire
still uses a silent unwrap_or fallback, unlike publisher_try_acquire which now
logs and returns false on query failure. Update player_try_acquire to handle the
query_row error explicitly: log the database error with clear context using the
same error-reporting style as publisher_try_acquire, then fail closed by
returning false or the capped result as appropriate. Keep the change localized
around the active count query and its surrounding logic so the two acquire paths
behave consistently.
src/http.rs (1)

773-787: 🩺 Stability & Availability | 🔵 Trivial | ⚖️ Poor tradeoff

Timeout path silently re-enables a stream after a 202 "deleting" was returned.

If live connections don't drain within 30s, wait_and_finalize_stream_delete re-enables the stream and drops it from deleted_streams. The client already received 202 {status:"deleting"} and has no signal that the delete was abandoned—polling will show the stream alive again, which is surprising. Separately, the tokio::spawned finalizer is in-memory only: a process restart while a delete is pending leaves the stream disabled and stuck in deleted_streams with no task to finalize or revert it. Consider a durable/reconciled path or at least documenting the timeout-revert contract.

🤖 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 773 - 787, The timeout branch in
wait_and_finalize_stream_delete currently re-enables the stream and removes it
from deleted_streams after returning 202 deleting, which makes the delete appear
to vanish; change the flow so a timed-out delete remains in a clear
terminal/pending-failed state or is explicitly reconciled, rather than silently
restoring the stream. Update finalize_stream_delete, deleted_streams handling,
and the delete path in src/http.rs so clients can observe the abandoned delete
consistently. Also address the in-memory tokio::spawn finalizer limitation by
persisting enough delete state in AppState/db to recover after restart or
document and enforce the timeout-revert contract explicitly.
src/server.rs (1)

726-740: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Global env var mutation in a parallel test can leak into sibling tests.

std::env::set_var/remove_var mutate process-wide state; cargo test runs tests in parallel by default, and the sibling create_generates_api_token_on_first_start test also calls ServerApp::bootstrap, which reads LRTMP2_API_TOKEN. A concurrent run risks flaky cross-test contamination, and a panic between set_var/remove_var (e.g. a failed assert_eq!) leaves the var set for the rest of the process.

Consider guarding these tests with a shared mutex/serial_test, or refactoring resolve_api_token to accept an injectable token source for testing instead of reading std::env directly.

🤖 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/server.rs` around lines 726 - 740, The
`bootstrap_seeds_api_token_from_env_on_first_start` test mutates the
process-wide `LRTMP2_API_TOKEN` env var, which can leak into other parallel
tests that also call `ServerApp::bootstrap`. Update the test setup around
`std::env::set_var`/`remove_var` to run serially or under a shared lock, or
refactor `resolve_api_token`/`ServerApp::bootstrap` to accept an injectable
token source so tests do not depend on global env state.
🤖 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/http.rs`:
- Around line 504-519: The unauthenticated health check in handle_health now
returns only the minimal status payload, so the e2e test
health_endpoint_reports_rtmp_port is asserting fields that are no longer
present. Update the test to either include Bearer authorization when calling
/api/v1/health so it can assert rtmp_port and the other detailed fields, or
change its expectation to match the unauthenticated {"status":"ok"} response.

In `@src/rate_limit.rs`:
- Around line 193-201: The rate limit eviction test is flaky because
`limited_key` ties with every other bucket on the same seed timestamp, and
`min_by_key` can evict it nondeterministically based on `HashMap` iteration
order. Update the setup in the `rate_limit` test so `limited_key` is clearly the
newest entry (for example by giving it a later timestamp than the others) before
calling `limiter.check`. Keep the existing assertions, but ensure the eviction
target is deterministic so `check(limited_key, max)` always remains rejected
after the full map eviction.
- Around line 45-55: The eviction logic in evict_oldest_client currently uses
entries.first(), which treats the first retained request as the activity
timestamp and can evict an actively used client bucket incorrectly. Update the
min_by_key comparison to use entries.last() so the HashMap<String, Vec<Instant>>
is ordered by each bucket’s most recent activity, matching the
least-recently-seen policy.

In `@src/rtmp_bridge.rs`:
- Around line 116-145: The per-connection auth throttling in rtmp_bridge.rs is
reset too easily because auth_failures is keyed by ConnId and cleared by
clear_auth_failures/on_close, allowing reconnects to bypass the window. Update
is_auth_rate_limited, record_auth_failure, and clear_auth_failures to track
failures by a stable client identity such as remote_addr (or another persistent
key) instead of ConnId, or otherwise keep the failure history alive for the full
RTMP_AUTH_FAILURE_WINDOW after disconnect so reconnects do not reset the limit.

In `@src/server.rs`:
- Around line 329-333: The validation error message in the
`is_valid_env_api_token` check is incomplete and should match the actual token
rules. Update the `Err(...)` text in `src/server.rs` where the token is rejected
so it explicitly says the token may contain ASCII alphanumeric characters plus
`-` and `_`, keeping the wording aligned with `is_valid_env_api_token` and the
surrounding API token validation logic.
- Around line 345-355: The bootstrap banner in key generation is currently
printing a masked value, which prevents the operator from copying the newly
generated API token. In the token setup flow around keygen_api_token() and
db.token_set(), either log the actual candidate once in this eprintln! path or
update the message to direct the operator to a different retrieval method, and
keep mask_api_token() only for non-sensitive display paths.

---

Nitpick comments:
In `@src/db.rs`:
- Around line 828-834: The COUNT query in player_try_acquire still uses a silent
unwrap_or fallback, unlike publisher_try_acquire which now logs and returns
false on query failure. Update player_try_acquire to handle the query_row error
explicitly: log the database error with clear context using the same
error-reporting style as publisher_try_acquire, then fail closed by returning
false or the capped result as appropriate. Keep the change localized around the
active count query and its surrounding logic so the two acquire paths behave
consistently.

In `@src/http.rs`:
- Around line 773-787: The timeout branch in wait_and_finalize_stream_delete
currently re-enables the stream and removes it from deleted_streams after
returning 202 deleting, which makes the delete appear to vanish; change the flow
so a timed-out delete remains in a clear terminal/pending-failed state or is
explicitly reconciled, rather than silently restoring the stream. Update
finalize_stream_delete, deleted_streams handling, and the delete path in
src/http.rs so clients can observe the abandoned delete consistently. Also
address the in-memory tokio::spawn finalizer limitation by persisting enough
delete state in AppState/db to recover after restart or document and enforce the
timeout-revert contract explicitly.

In `@src/server.rs`:
- Around line 726-740: The `bootstrap_seeds_api_token_from_env_on_first_start`
test mutates the process-wide `LRTMP2_API_TOKEN` env var, which can leak into
other parallel tests that also call `ServerApp::bootstrap`. Update the test
setup around `std::env::set_var`/`remove_var` to run serially or under a shared
lock, or refactor `resolve_api_token`/`ServerApp::bootstrap` to accept an
injectable token source so tests do not depend on global env state.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 58144bda-e657-4442-890a-80cddf240a9e

📥 Commits

Reviewing files that changed from the base of the PR and between 42a642b and 595a97e.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • .env.example
  • src/config.rs
  • src/db.rs
  • src/http.rs
  • src/rate_limit.rs
  • src/rtmp_bridge.rs
  • src/server.rs

Comment thread src/http.rs Outdated
Comment thread src/rate_limit.rs
Comment thread src/rate_limit.rs
Comment thread src/rtmp_bridge.rs Outdated
Comment thread src/server.rs
Comment thread src/server.rs

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 723d696cdc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/http.rs
Comment on lines +842 to +845
tokio::spawn(async move {
wait_and_finalize_stream_delete(state_bg, id_bg).await;
});
(StatusCode::ACCEPTED, Json(json!({"status": "deleting"}))).into_response()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid leaving abandoned deletes disabled

When deleting a stream with active RTMP sessions, the handler has already persisted enabled=0 and then returns 202 while relying on this detached task to either delete the row or re-enable it. If the process is shut down, redeployed, or crashes before the spawned task completes, Tokio drops the task and the next startup only clears active publisher/player rows, leaving the stream disabled in SQLite but not deleted; make the delete/re-enable outcome durable or recover pending deletes on startup before acknowledging the request.

Useful? React with 👍 / 👎.

- Rate limiter LRU eviction now keys on the most recent request per
  bucket (entries.last()) instead of the oldest, so active clients
  can't be evicted ahead of stale ones; fix the matching test to give
  the protected bucket an unambiguously newer timestamp.
- Print the freshly generated API token in the first-startup banner
  instead of the masked form, since there is no other way for an
  operator to retrieve it.
- Clarify that LRTMP2_API_TOKEN in .env.example is not read from the
  file by the config loader and must be a real process env var.
- Key RTMP auth-failure rate limiting by client IP instead of ConnId,
  and stop clearing the failure history on connection close, so
  reconnecting no longer resets the brute-force window.
- Recover streams left disabled-but-undeleted by a crash mid-delete
  (the async 202 delete path) by finishing the delete on startup.
- Fix the LRTMP2_API_TOKEN validation error message to mention the
  allowed '-'/'_' characters.

@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: 4

🧹 Nitpick comments (1)
src/server.rs (1)

763-770: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Serialize this test and make env cleanup panic-safe. std::env::set_var/remove_var are process-global, and other tests in src/server.rs also call ServerApp::bootstrap in the same test binary. Use the existing serial_test crate (or a shared Mutex) and restore the prior value with an RAII guard so a panic doesn’t leak LRTMP2_API_TOKEN.

🤖 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/server.rs` around lines 763 - 770, This test in ServerApp::bootstrap
mutates the process-global LRTMP2_API_TOKEN without isolation or panic-safe
cleanup. Serialize the test using the existing serial_test crate (or a shared
Mutex) so it cannot run alongside other src/server.rs tests, and replace the
manual set_var/remove_var pair with an RAII guard that captures and restores the
previous env var value even if the test panics.
🤖 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/rtmp_bridge.rs`:
- Around line 615-650: The brute-force tracking in authorize_publish and
authorize_play is too broad because every Err(()) from
try_authorize_publish/try_authorize_play is treated as an auth failure. Update
the authorization flow to distinguish credential/app mismatches from operational
or capacity errors (such as deleted streams, capacity limits, keygen, or DB
acquisition failures), and only call record_auth_failure for true auth-related
rejections while still clearing failures on success.
- Around line 615-650: In authorize_publish and authorize_play, avoid using an
empty string as the fallback remote IP before on_connect has populated
ConnState. The shared "" bucket causes unrelated connections to share
auth-failure/rate-limit state and clear each other’s failures, so update the
remote_ip lookup from self.conns to use a per-connection placeholder or bypass
auth throttling until the real IP is available. Keep the fix localized around
authorize_publish, authorize_play, and the auth-failure helpers they call.
- Around line 136-165: The auth-failure cache in
rtmp_bridge::is_auth_rate_limited and record_auth_failure only removes expired
entries for the current IP, so unused buckets in auth_failures can accumulate
indefinitely. Add a global cleanup/prune step around the auth_failures lock
before inserting or when checking failures, and consider enforcing a maximum
bucket count so stale remote_ip entries are evicted even if they are never
revisited. Keep the existing per-IP expiry logic in is_auth_rate_limited, but
make the pruning apply across the whole auth_failures map.
- Around line 734-765: The test auth_rate_limit_survives_reconnect_from_same_ip
currently only checks authorize_publish returning Err, which does not prove
throttling because bogus keys fail for other reasons too. Update the test to
assert the actual rate-limit state via is_auth_rate_limited using the remote IP
for the relevant connection, or switch the final checks to a valid key so the
same IP is blocked while a different IP is not. Use the existing test_bridge,
authorize_publish, on_connect, and on_close flow to keep the setup the same.

---

Nitpick comments:
In `@src/server.rs`:
- Around line 763-770: This test in ServerApp::bootstrap mutates the
process-global LRTMP2_API_TOKEN without isolation or panic-safe cleanup.
Serialize the test using the existing serial_test crate (or a shared Mutex) so
it cannot run alongside other src/server.rs tests, and replace the manual
set_var/remove_var pair with an RAII guard that captures and restores the
previous env var value even if the test panics.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: db3ff218-5646-43ed-b510-69e63c705cf8

📥 Commits

Reviewing files that changed from the base of the PR and between 723d696 and 06176b0.

📒 Files selected for processing (4)
  • .env.example
  • src/rate_limit.rs
  • src/rtmp_bridge.rs
  • src/server.rs
✅ Files skipped from review due to trivial changes (1)
  • .env.example
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/rate_limit.rs

Comment thread src/rtmp_bridge.rs
Comment thread src/rtmp_bridge.rs Outdated
Comment thread src/rtmp_bridge.rs

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 06176b0bf8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/rtmp_bridge.rs
Comment on lines +616 to +621
let remote_ip = self
.conns
.lock()
.get(&conn)
.map(|cs| cs.remote_ip.clone())
.unwrap_or_default();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid global RTMP auth buckets before peer IP is known

When the RTMP publish/play callback fires before on_connect has populated ConnState for this connection, this falls back to an empty peer key and records failures under "". Because this ordering is explicitly supported by the bridge, ten invalid early callbacks from any clients make is_auth_rate_limited("") reject subsequent early publish/play attempts from unrelated IPs for the 60-second window, turning the intended per-peer throttle into a global RTMP auth denial; skip accounting until the remote IP is known or attach the peer address before authorization.

Useful? React with 👍 / 👎.

Comment thread src/rtmp_bridge.rs
fn record_auth_failure(&self, remote_ip: &str) {
let mut guard = self.auth_failures.lock();
let now = Instant::now();
let entries = guard.entry(remote_ip.to_string()).or_default();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Cap or purge RTMP auth failure buckets

Each failing RTMP peer creates an auth_failures bucket, but stale buckets are only pruned when that same peer fails or is checked again, and there is no global cap like the HTTP limiter has. On a long-lived public RTMP server, one-off failed auth attempts from many client IPs will leave permanent HashMap entries after their 60-second windows expire, so a scan can grow memory without bound; periodically purge all buckets or evict old peers before inserting a new key.

Useful? React with 👍 / 👎.

Comment thread src/server.rs Outdated
Comment on lines +316 to +320
for stream in db.stream_list() {
if stream.enabled {
continue;
}
match db.stream_delete(&stream.id) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not delete every disabled stream on startup

This recovery treats every persisted enabled=0 stream as an abandoned delete and removes it. If an operator or future admin path uses the existing enabled column to keep a stream configured but disabled (the API serializes this field and RTMP auth already rejects disabled streams without deleting them), the next server restart will silently cascade-delete that stream and its keys; track pending deletes separately instead of inferring them from enabled=false.

Useful? React with 👍 / 👎.

- Distinguish credential/app-mismatch auth failures (brute-forceable)
  from operational rejections (deleted/disabled stream, capacity,
  DB/keygen errors) so only the former count against the RTMP
  auth-failure rate limit.
- Skip auth-failure throttling entirely when authorize_publish/play
  runs before on_connect has recorded a remote IP, instead of sharing
  a "" bucket across unrelated connections.
- Bound auth_failures with a global prune + LRU eviction cap
  (MAX_TRACKED_AUTH_FAILURE_KEYS), mirroring the HTTP rate limiter.
- Track pending stream deletes with a dedicated `pending_delete`
  column instead of inferring them from `enabled=0`, so an
  administratively-disabled-but-not-deleted stream is never
  cascade-deleted by the startup recovery pass.
- Tighten the reconnect-throttle regression test to assert
  is_auth_rate_limited directly instead of only checking for an Err
  that a bogus key would produce either way.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@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/rtmp_bridge.rs`:
- Around line 462-465: The publish rejection path in rtmp_bridge.rs is treating
disabled or delete-drain streams as Credential failures, which wrongly consumes
the shared auth-failure budget. Update the stream authorization logic around
stream_find_by_publish_key() to distinguish invalid keys from
disabled/pending-delete streams, and mirror try_authorize_play() by returning
AuthFailureKind::Operational for the disabled/delete-drain cases while keeping
true bad-key cases as Credential.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8d1bbac6-1964-4812-ad91-76795f57d422

📥 Commits

Reviewing files that changed from the base of the PR and between 06176b0 and d4ede1c.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • src/db.rs
  • src/rtmp_bridge.rs
  • src/server.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/server.rs

Comment thread src/rtmp_bridge.rs Outdated
stream_find_by_publish_key() filters WHERE enabled=1, so a publisher
retrying with a still-valid key against its own just-disabled or
pending-delete stream fell into the "invalid publish_key" branch and
was counted as a Credential auth failure — burning the shared per-IP
brute-force budget for a rejection that has nothing to do with
guessing a key. Add stream_find_by_publish_key_any() (same query
without the enabled filter) and check `enabled` explicitly in
try_authorize_publish, mirroring the existing check already present in
try_authorize_play.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@AlexanderWagnerDev AlexanderWagnerDev merged commit 37f2c24 into main Jul 7, 2026
11 checks passed
@AlexanderWagnerDev AlexanderWagnerDev deleted the cursor/security-review-fixes branch July 7, 2026 19:20
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.

2 participants