Skip to content

πŸ›‘οΈ Sentinel: [HIGH] Enforce safeFetch for downloader API connections to prevent SSRF - #889

Open
Doezer wants to merge 1 commit into
mainfrom
sentinel-safefetch-downloaders-1321208409107426218
Open

πŸ›‘οΈ Sentinel: [HIGH] Enforce safeFetch for downloader API connections to prevent SSRF#889
Doezer wants to merge 1 commit into
mainfrom
sentinel-safefetch-downloaders-1321208409107426218

Conversation

@Doezer

@Doezer Doezer commented Aug 5, 2026

Copy link
Copy Markdown
Owner

🚨 Severity: HIGH
πŸ’‘ Vulnerability: Downloader API communication (e.g. qBittorrent, Transmission, NZBGet, Deluge, rTorrent) was using the native fetch() to call external URLs (e.g. downloaders running locally or remotely), making the application vulnerable to Server-Side Request Forgery (SSRF) and DNS Rebinding via metadata or local networks.
🎯 Impact: Attackers could potentially exploit this to access sensitive hostnames or local/cloud metadata networks (169.254.169.254, etc.) via DNS rebinding if they could control the configured downloader URLs.
πŸ”§ Fix: Replaced the native fetch() calls in the downloader client implementations with the secure safeFetch wrapper from server/ssrf.ts to ensure strict host-level validation of all connections. Additionally, updated the test suites to properly mock safeFetch.
βœ… Verification: Ran the full test suite (npm run test) and verified that all tests passed successfully, confirming the intended behavior without regressions.


PR created automatically by Jules for task 1321208409107426218 started by @Doezer

Summary by CodeRabbit

  • Security

    • Protected downloader connections against unsafe requests to local networks and cloud metadata services.
    • Applied secure request handling across Deluge, NZBGet, qBittorrent, rTorrent, and Transmission integrations.
  • Tests

    • Updated downloader and route tests to cover secure request handling and preserve existing request behavior.
  • Documentation

    • Added guidance documenting SSRF risks and secure requirements for external downloader connections.

Co-authored-by: Doezer <11655673+Doezer@users.noreply.github.com>
@google-labs-jules

Copy link
Copy Markdown
Contributor

πŸ‘‹ Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a πŸ‘€ emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@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.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

πŸ“ Walkthrough

Walkthrough

Downloader API requests now use safeFetch instead of native fetch. Test mocks forward requests through global fetch, and sentinel documentation records SSRF risks in downloader requests.

Changes

SSRF-safe downloader routing

Layer / File(s) Summary
Primary downloader request routing
server/downloaders/deluge.ts, server/downloaders/nzbget.ts, server/downloaders/transmission.ts
Deluge, NZBGet, and Transmission route RPC requests through safeFetch while preserving request options and response handling.
Authentication and retry routing
server/downloaders/qbittorrent.ts, server/downloaders/rtorrent.ts
qBittorrent and rTorrent use safeFetch for authentication, initial requests, and authentication retries.
SSRF test mock integration
server/__tests__/downloaders_*.test.ts, server/__tests__/routes.test.ts
Test mocks provide safeFetch and forward its URL and request options to the global fetch mock.
SSRF prevention documentation
.jules/sentinel.md
The documentation records a fixed prevention date and SSRF and DNS-rebinding exposure in downloader API requests.

Estimated code review effort: 3 (Moderate) | ~20 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 clearly summarizes the main change: enforcing safeFetch for downloader API connections to prevent SSRF.
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 sentinel-safefetch-downloaders-1321208409107426218

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❀️ Share

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

@sonarqubecloud

sonarqubecloud Bot commented Aug 5, 2026

Copy link
Copy Markdown

@codecov

codecov Bot commented Aug 5, 2026

Copy link
Copy Markdown

Codecov Report

βœ… All modified and coverable lines are covered by tests.
βœ… All tests successful. No failed tests found.

πŸ“’ Thoughts on this report? Let us know!

@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

πŸ€– 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 @.jules/sentinel.md:
- Line 31: Insert one blank line immediately after the β€œ2025-05-24 - SSRF
Prevention in Internal Downstream Requests” heading in the changelog section,
before its following paragraph.

In `@server/__tests__/downloaders_deluge.test.ts`:
- Line 22: Centralize the shared SSRF test mocks in tests/setup.ts: move the
isSafeUrl and safeFetch mock definitions there, then remove them from
server/__tests__/downloaders_deluge.test.ts#L22-L22,
server/__tests__/downloaders_deluge_coverage.test.ts#L22-L22,
server/__tests__/downloaders_deluge_remaining.test.ts#L21-L24,
server/__tests__/downloaders_rtorrent_remaining.test.ts#L25-L25,
server/__tests__/downloaders_transmission_remaining.test.ts#L24-L24, and
server/__tests__/routes.test.ts#L4-L7. Retain only test-specific local overrides
in each file.
- Line 22: Pass-through safeFetch mocks bypass wrapper protections, leaving
validation, redirect revalidation, DNS-rebinding, timeout, and
private/unsafe-redirect behavior untested. In
server/__tests__/downloaders_deluge.test.ts:22,
server/__tests__/downloaders_deluge_coverage.test.ts:22,
server/__tests__/downloaders_deluge_remaining.test.ts:21-24,
server/__tests__/downloaders_rtorrent_remaining.test.ts:25, and
server/__tests__/downloaders_transmission_remaining.test.ts:24, add coverage
using the real safeFetch wrapper for the respective downloader paths. In
server/__tests__/routes.test.ts:4-7, add route-level coverage for private and
unsafe-redirect rejection; retain pass-through mocks only where wrapper controls
are not under test.

In `@server/__tests__/downloaders_helpers_regression.test.ts`:
- Around line 31-35: Update the SSRF mocks and downloader assertions in
server/__tests__/downloaders_helpers_regression.test.ts (lines 31-35),
server/__tests__/downloaders_nzbget_remaining.test.ts (line 17), and
server/__tests__/downloaders_qbittorrent_remaining.test.ts (line 24): import and
spy on safeFetch, assert each downloader invokes it rather than only
global.fetch or fetchMock, and add separate coverage for private-URL and
redirect validation paths.
πŸͺ„ Autofix

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: ASSERTIVE

Plan: Pro Plus

Run ID: d60fd4f4-b45f-4b80-84a7-d650b7d161c3

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 8692f4d and 5248312.

πŸ“’ Files selected for processing (15)
  • .jules/sentinel.md
  • server/__tests__/downloaders_deluge.test.ts
  • server/__tests__/downloaders_deluge_coverage.test.ts
  • server/__tests__/downloaders_deluge_remaining.test.ts
  • server/__tests__/downloaders_helpers_regression.test.ts
  • server/__tests__/downloaders_nzbget_remaining.test.ts
  • server/__tests__/downloaders_qbittorrent_remaining.test.ts
  • server/__tests__/downloaders_rtorrent_remaining.test.ts
  • server/__tests__/downloaders_transmission_remaining.test.ts
  • server/__tests__/routes.test.ts
  • server/downloaders/deluge.ts
  • server/downloaders/nzbget.ts
  • server/downloaders/qbittorrent.ts
  • server/downloaders/rtorrent.ts
  • server/downloaders/transmission.ts

Comment thread .jules/sentinel.md
**Learning:** Checking string prefixes for URL validation is fundamentally insecure because parts of the prefix might be interpreted as the username/password in a URL with a different domain. Attackers can leverage this to bypass domain allowlists.
**Prevention:** Always use the `URL` object (e.g., `new URL()`) to parse URLs and explicitly validate the `hostname` and `pathname` properties instead of checking raw string prefixes.

## 2025-05-24 - SSRF Prevention in Internal Downstream Requests

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

πŸ“ Maintainability & Code Quality | 🟑 Minor | ⚑ Quick win

Add a blank line after the heading.

Markdownlint reports MD022 because Line [31] is immediately followed by the paragraph on Line [32]. Insert one blank line.

🧰 Tools
πŸͺ› markdownlint-cli2 (0.23.2)

[warning] 31-31: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

πŸ€– 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 @.jules/sentinel.md at line 31, Insert one blank line immediately after the
β€œ2025-05-24 - SSRF Prevention in Internal Downstream Requests” heading in the
changelog section, before its following paragraph.

Source: Linters/SAST tools


vi.mock("../ssrf.js", () => ({
isSafeUrl: vi.fn().mockResolvedValue(true),
safeFetch: vi.fn((url: string, options: RequestInit) => fetch(url, options)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

πŸ“ Maintainability & Code Quality | 🟠 Major | πŸ—οΈ Heavy lift

Centralize the repeated SSRF test setup. The same mock is defined in six files. Move it to tests/setup.ts, and retain only test-specific overrides locally.

  • server/__tests__/downloaders_deluge.test.ts#L22-L22: move the safeFetch mock to shared setup.
  • server/__tests__/downloaders_deluge_coverage.test.ts#L22-L22: move the safeFetch mock to shared setup.
  • server/__tests__/downloaders_deluge_remaining.test.ts#L21-L24: move the isSafeUrl and safeFetch mocks to shared setup.
  • server/__tests__/downloaders_rtorrent_remaining.test.ts#L25-L25: move the safeFetch mock to shared setup.
  • server/__tests__/downloaders_transmission_remaining.test.ts#L24-L24: move the safeFetch mock to shared setup.
  • server/__tests__/routes.test.ts#L4-L7: move the isSafeUrl and safeFetch mocks to shared setup.

As per coding guidelines, shared test setup must use tests/setup.ts.

πŸ“ Affects 6 files
  • server/__tests__/downloaders_deluge.test.ts#L22-L22 (this comment)
  • server/__tests__/downloaders_deluge_coverage.test.ts#L22-L22
  • server/__tests__/downloaders_deluge_remaining.test.ts#L21-L24
  • server/__tests__/downloaders_rtorrent_remaining.test.ts#L25-L25
  • server/__tests__/downloaders_transmission_remaining.test.ts#L24-L24
  • server/__tests__/routes.test.ts#L4-L7
πŸ€– 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 `@server/__tests__/downloaders_deluge.test.ts` at line 22, Centralize the
shared SSRF test mocks in tests/setup.ts: move the isSafeUrl and safeFetch mock
definitions there, then remove them from
server/__tests__/downloaders_deluge.test.ts#L22-L22,
server/__tests__/downloaders_deluge_coverage.test.ts#L22-L22,
server/__tests__/downloaders_deluge_remaining.test.ts#L21-L24,
server/__tests__/downloaders_rtorrent_remaining.test.ts#L25-L25,
server/__tests__/downloaders_transmission_remaining.test.ts#L24-L24, and
server/__tests__/routes.test.ts#L4-L7. Retain only test-specific local overrides
in each file.

Source: Coding guidelines


πŸ”’ Security & Privacy | πŸ”΅ Trivial | πŸ—οΈ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rtk rg -n 'safeFetch|isSafeUrl|DNS rebinding|private|redirect|timeout' server --glob '*.test.ts'

Repository: Doezer/Questarr

Length of output: 194


🏁 Script executed:

#!/bin/bash
set -u

echo "== repository files around tests/implementation =="
git ls-files | rg '(^|/)server/(ssrf\.ts|downloaders/.*\.ts|__tests__/.*\.test\.ts|routes\.test\.ts)$' | sed -n '1,120p'

echo ""
echo "== search SSRF terms in tracked server tests without rtk =="
rg -n 'safeFetch|isSafeUrl|DNS rebinding|private|redirect|timeout' server --glob '*.test.ts' || true

echo ""
echo "== relevant test imports/mock snippets =="
for f in \
  server/__tests__/downloaders_deluge.test.ts \
  server/__tests__/downloaders_deluge_coverage.test.ts \
  server/__tests__/downloaders_deluge_remaining.test.ts \
  server/__tests__/downloaders_rtorrent_remaining.test.ts \
  server/__tests__/downloaders_transmission_remaining.test.ts \
  server/__tests__/routes.test.ts
do
  if [ -f "$f" ]; then
    echo "--- $f ($(wc -l < "$f") lines) ---"
    sed -n '1,80p' "$f"
  fi
done

echo ""
echo "== ssrf implementation outline and content =="
wc -l server/ssrf.ts
sed -n '1,260p' server/ssrf.ts

Repository: Doezer/Questarr

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -u

echo "== targeted test snippets =="
for f in \
  server/__tests__/downloaders_deluge.test.ts \
  server/__tests__/downloaders_deluge_coverage.test.ts \
  server/__tests__/downloaders_deluge_remaining.test.ts \
  server/__tests__/downloaders_rtorrent_remaining.test.ts \
  server/__tests__/downloaders_transmission_remaining.test.ts \
  server/__tests__/routes.test.ts
do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    sed -n '1,120p' "$f"
  fi
done

echo ""
echo "== targeted ssrf implementation =="
sed -n '1,240p' server/ssrf.ts

echo ""
echo "== targeted existing SSRF tests snippets =="
for f in server/__tests__/ssrf.test.ts server/__tests__/downloaders_ssrf.test.ts server/__tests__/rss-ssrf.test.ts server/__tests__/ssrf_routes.test.ts; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    sed -n '1,220p' "$f"
  fi
done

echo ""
echo "== safeFetch call sites in listed targeted files =="
python3 - <<'PY'
from pathlib import Path
paths = [
 "server/__tests__/downloaders_deluge.test.ts",
 "server/__tests__/downloaders_deluge_coverage.test.ts",
 "server/__tests__/downloaders_deluge_remaining.test.ts",
 "server/__tests__/downloaders_rtorrent_remaining.test.ts",
 "server/__tests__/downloaders_transmission_remaining.test.ts",
 "server/__tests__/routes.test.ts",
 "server/ssrf.ts",
]
for p in paths:
    text = Path(p).read_text()
    print(f"--- {p} contains safeFetch/isSafeUrl/redirect/timeout/private/DNS occurrences ---")
    for i, line in enumerate(text.splitlines(), 1):
        lower = line.lower()
        if any(x in lower for x in ["safefetch", "issafeurl", "redirect", "timeout", "private", "dns", "metadata", "rebind"]):
            print(f"{i}: {line}")
PY

Repository: Doezer/Questarr

Length of output: 50371


Other (CWE-693)

Reachability path
● Entry
  server/__tests__/downloaders_deluge.test.ts
β”‚
β–Ό
● Sink
  server/downloaders/deluge.ts

Cover safeFetch controls outside pass-through mock tests. The listed tests replace safeFetch with native fetch, so they no longer test URL validation, redirect revalidation, DNS-rebinding checks, or timeouts for those paths. Add coverage in tests that use the real wrapper for Deluge, rTorrent, Transmission, and route-level private/unsafe-redirect rejection.

πŸ“ Affects 6 files
  • server/__tests__/downloaders_deluge.test.ts#L22-L22 (this comment)
  • server/__tests__/downloaders_deluge_coverage.test.ts#L22-L22
  • server/__tests__/downloaders_deluge_remaining.test.ts#L21-L24
  • server/__tests__/downloaders_rtorrent_remaining.test.ts#L25-L25
  • server/__tests__/downloaders_transmission_remaining.test.ts#L24-L24
  • server/__tests__/routes.test.ts#L4-L7
πŸ€– 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 `@server/__tests__/downloaders_deluge.test.ts` at line 22, Pass-through
safeFetch mocks bypass wrapper protections, leaving validation, redirect
revalidation, DNS-rebinding, timeout, and private/unsafe-redirect behavior
untested. In server/__tests__/downloaders_deluge.test.ts:22,
server/__tests__/downloaders_deluge_coverage.test.ts:22,
server/__tests__/downloaders_deluge_remaining.test.ts:21-24,
server/__tests__/downloaders_rtorrent_remaining.test.ts:25, and
server/__tests__/downloaders_transmission_remaining.test.ts:24, add coverage
using the real safeFetch wrapper for the respective downloader paths. In
server/__tests__/routes.test.ts:4-7, add route-level coverage for private and
unsafe-redirect rejection; retain pass-through mocks only where wrapper controls
are not under test.

Comment on lines +31 to +35
vi.mock("../ssrf.js", () => ({
isSafeUrl: vi.fn().mockResolvedValue(true),
safeFetch: vi.fn((url: string, options?: RequestInit) => global.fetch(url, options)),
resolveSafeAddress: vi.fn().mockResolvedValue({ address: "127.0.0.1", family: 4 }),
}));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | πŸ”΅ Trivial | ⚑ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rtk rg -n -C 6 'safeFetch|global\.fetch|fetchMock|isSafeUrl' server/__tests__

Repository: Doezer/Questarr

Length of output: 194


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "repo files matching test target:"
git ls-files 'server/__tests__/*' | sed -n '1,200p'

echo
echo "security/ssrf.ts:"
if [ -f server/ssrf.ts ]; then wc -l server/ssrf.ts; sed -n '1,240p' server/ssrf.ts; fi

echo
echo "mocks and safeFetch references in tests (without rtk):"
rg -n -C 4 'safeFetch|global\.fetch|fetchMock|isSafeUrl|resolveSafeAddress' server/__tests__ || true

Repository: Doezer/Questarr

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "ssrf.ts:"
if [ -f server/ssrf.ts ]; then
  wc -l server/ssrf.ts
  sed -n '1,260p' server/ssrf.ts
fi

echo
echo "focused test files:"
for f in server/__tests__/downloaders_helpers_regression.test.ts server/__tests__/downloaders_nzbget_remaining.test.ts server/__tests__/downloaders_qbittorrent_remaining.test.ts; do
  if [ -f "$f" ]; then
    echo "--- $f"
    wc -l "$f"
    sed -n '1,220p' "$f"
  fi
done

echo
echo "focused safeFetch/imported test assertions:"
python3 - <<'PY'
from pathlib import Path
files = [
  Path("server/__tests__/downloaders_helpers_regression.test.ts"),
  Path("server/__tests__/downloaders_nzbget_remaining.test.ts"),
  Path("server/__tests__/downloaders_qbittorrent_remaining.test.ts"),
]
for p in files:
    if not p.exists():
        continue
    text = p.read_text()
    print(f"{p}: safeFetch_imported={'.safeFetch' in text}; safeFetch_expected={text.count('safeFetch')}")
PY

Repository: Doezer/Questarr

Length of output: 29698


Assert safeFetch usage in these test mocks.

The mock aliases let regressions to native fetch still pass. In these files, import safeFetch where used and verify the downloader path runs through it instead of relying only on global.fetch/fetchMock. Also cover private URL and redirect checks separately.

πŸ“ Affects 3 files
  • server/__tests__/downloaders_helpers_regression.test.ts#L31-L35 (this comment)
  • server/__tests__/downloaders_nzbget_remaining.test.ts#L17-L17
  • server/__tests__/downloaders_qbittorrent_remaining.test.ts#L24-L24
πŸ€– 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 `@server/__tests__/downloaders_helpers_regression.test.ts` around lines 31 -
35, Update the SSRF mocks and downloader assertions in
server/__tests__/downloaders_helpers_regression.test.ts (lines 31-35),
server/__tests__/downloaders_nzbget_remaining.test.ts (line 17), and
server/__tests__/downloaders_qbittorrent_remaining.test.ts (line 24): import and
spy on safeFetch, assert each downloader invokes it rather than only
global.fetch or fetchMock, and add separate coverage for private-URL and
redirect validation paths.

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.

1 participant