Skip to content

fix: prevent SSRF and open redirect in alert targets and OIDC logout#1615

Open
nikhilsinhaparseable wants to merge 1 commit intoparseablehq:mainfrom
nikhilsinhaparseable:fix-security
Open

fix: prevent SSRF and open redirect in alert targets and OIDC logout#1615
nikhilsinhaparseable wants to merge 1 commit intoparseablehq:mainfrom
nikhilsinhaparseable:fix-security

Conversation

@nikhilsinhaparseable
Copy link
Copy Markdown
Contributor

@nikhilsinhaparseable nikhilsinhaparseable commented Apr 8, 2026

Add endpoint validation for alert targets to block SSRF attacks.

  • Validates scheme (http/https only)
  • rejects private/loopback/link-local
  • IPs for both literal and DNS-resolved addresses, including IPv4-mapped IPv6 and 0.0.0.0/8
  • DNS resolution is re-validated and pinned at request time via reqwest's resolve() to prevent DNS rebinding.

Validate redirect URL in OIDC logout to prevent open redirect attacks.

Summary by CodeRabbit

  • New Features

    • Enhanced validation for alert target endpoints
    • Implemented DNS pinning for outgoing alert requests to improve security
  • Bug Fixes

    • Improved redirect URL validation in OIDC logout handler

Add endpoint validation for alert targets to block SSRF attacks.
- Validates scheme (http/https only)
- rejects private/loopback/link-local
- IPs for both literal and DNS-resolved addresses, including IPv4-mapped
  IPv6 and 0.0.0.0/8
- DNS resolution is re-validated and pinned at request time via reqwest's
  resolve() to prevent DNS rebinding.

Validate redirect URL in OIDC logout to prevent open redirect attacks.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 8, 2026

Walkthrough

The changes introduce SSRF and DNS-rebinding protection mechanisms for alert target endpoints by adding validation and DNS pinning helpers, integrate endpoint validation into target creation and update handlers, and enhance the OIDC logout handler with redirect URL validation and error result type wrapping.

Changes

Cohort / File(s) Summary
SSRF/DNS-Rebinding Protection
src/alerts/target.rs
Added endpoint_url() method on TargetType and introduced validation helpers: mask_endpoint() for logging redaction, is_private_ip() for IP range classification, validate_target_endpoint() for endpoint scheme/IP validation, resolve_and_pin() for DNS resolution, and apply_dns_pinning() for DNS pinning via ClientBuilder. Updated SlackWebHook::call, OtherWebHook::call, and AlertManager::call to validate and pin DNS before sending requests.
Target Endpoint Validation Integration
src/handlers/http/targets.rs
Imported validate_target_endpoint and added async validation calls in both post and update handlers before persisting targets via TARGETS.update(...).
OIDC Logout Handler Error Handling
src/handlers/http/oidc.rs
Changed logout return type to Result<HttpResponse, OIDCError>, added redirect URL validation using is_valid_redirect_url, and wrap responses in Ok(...) while rejecting invalid redirects with OIDCError::BadRequest.

Sequence Diagram

sequenceDiagram
    participant App as Application
    participant Handler as Target Handler
    participant Validator as Validator
    participant Resolver as DNS Resolver
    participant Client as HTTP Client
    participant Target as Target Endpoint

    App->>Handler: Create/Update Target
    Handler->>Validator: validate_target_endpoint(url)
    Validator->>Validator: Check scheme is HTTP(S)
    Validator->>Resolver: resolve_and_pin(url)
    alt Domain Host
        Resolver->>Resolver: DNS lookup
        Resolver->>Validator: Check resolved IP<br/>(not private/internal)
        Validator-->>Handler: Return socket address
    else IP Literal Host
        Resolver-->>Handler: Return None (no pinning)
    end
    Handler->>Client: apply_dns_pinning(ClientBuilder, url)
    Client->>Target: Send request with pinned resolver
    Target-->>Client: Response
    Client-->>Handler: Success/Error
    Handler-->>App: Target created/updated
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • hotfix for alerts #1283: Modifies src/alerts/target.rs and target endpoint handling (prior change converted endpoints to Url type and serialization; this PR builds upon it by adding endpoint URL exposure, DNS validation, and DNS pinning mechanisms).

Poem

🐰 With DNS pinning and IP checks tight,
We block the rebinds lurking in the night,
SSRF's snare shall trouble us no more,
Alert targets validated to the core! 🛡️

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main security-focused changes: preventing SSRF in alert targets and open redirect in OIDC logout.
Description check ✅ Passed The description covers the main goal and key changes (SSRF validation details, DNS pinning, OIDC redirect validation), but lacks the checklist items required by the template.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/handlers/http/targets.rs (1)

41-42: Keep the endpoint invariant below the HTTP layer.

These handlers now validate the URL, but src/alerts/target.rs::TargetConfigs::update() and TargetConfigs::load() still accept arbitrary endpoints. That means non-HTTP write paths and preexisting metastore entries bypass this guard until delivery time. Moving the check into the target/storage layer would keep the invariant consistent.

Also applies to: 93-94

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/handlers/http/targets.rs` around lines 41 - 42, The HTTP handler
currently validates endpoints but TargetConfigs::update() and
TargetConfigs::load() still accept arbitrary endpoint URLs, letting non-HTTP
write paths and existing metastore records bypass the invariant; move the
endpoint validation into the target/storage layer by calling the same validation
routine (or embedding its logic) inside TargetConfigs::update() and
TargetConfigs::load() so any write or load of a Target runs
validate_target_endpoint (or equivalent) and returns an error on invalid
endpoints, ensuring TARGETS.update(...) and any load from storage cannot produce
Targets with invalid endpoint URLs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/alerts/target.rs`:
- Around line 625-633: apply_dns_pinning currently only sets DNS resolution but
leaves reqwest's default redirect behavior enabled, which allows SSRF via
redirect responses; update apply_dns_pinning (which is used by SlackWebHook,
OtherWebHook, and AlertManager) to disable redirects on the ClientBuilder before
returning (set the builder's redirect policy to none via the reqwest redirect
Policy API), so the returned ClientBuilder has redirects disabled in addition to
DNS pinning; keep resolve_and_pin and existing host-resolution logic unchanged.

In `@src/handlers/http/oidc.rs`:
- Around line 174-181: Replace the request-derived host check with validation
against the configured public base URL: in the logout handler replace using
req.connection_info().host() / base_url_without_scheme and call
is_valid_redirect_url() with the application-configured external origin/public
base URL (the same helper used for login) when validating query.redirect,
returning the same OIDCError::BadRequest on failure; apply the identical change
to login() so both paths validate redirect URLs against the configured public
base URL rather than Host/X-Forwarded-Host.

---

Nitpick comments:
In `@src/handlers/http/targets.rs`:
- Around line 41-42: The HTTP handler currently validates endpoints but
TargetConfigs::update() and TargetConfigs::load() still accept arbitrary
endpoint URLs, letting non-HTTP write paths and existing metastore records
bypass the invariant; move the endpoint validation into the target/storage layer
by calling the same validation routine (or embedding its logic) inside
TargetConfigs::update() and TargetConfigs::load() so any write or load of a
Target runs validate_target_endpoint (or equivalent) and returns an error on
invalid endpoints, ensuring TARGETS.update(...) and any load from storage cannot
produce Targets with invalid endpoint URLs.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 37c4d920-f3ce-4956-8c8a-062ffe721fac

📥 Commits

Reviewing files that changed from the base of the PR and between 79656f6 and 3dcddd6.

📒 Files selected for processing (3)
  • src/alerts/target.rs
  • src/handlers/http/oidc.rs
  • src/handlers/http/targets.rs

Comment on lines +625 to +633
async fn apply_dns_pinning(
mut builder: ClientBuilder,
endpoint: &Url,
) -> Result<ClientBuilder, String> {
if let Some((host, addr)) = resolve_and_pin(endpoint).await? {
builder = builder.resolve(&host, addr);
}
Ok(builder)
}
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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Does reqwest::ClientBuilderfollow HTTP redirects by default for POST/307/308 responses, and isredirect(reqwest::redirect::Policy::none()) the supported way to disable them?

💡 Result:

Yes.

  • Default behavior: reqwest::ClientBuilder (and Client) uses redirect::Policy::default(), which automatically follows redirects up to 10 hops. This applies to redirects in general, including 307/308 responses. [1], [2]
  • POST + 307/308: For 307/308, the redirect must be able to re-send the original request body (i.e., the body must be clonable). Reqwest’s redirect implementation explicitly notes it must be able to clone the body “to make 307 and 308 redirects work”, meaning it will follow them when it can reissue the POST with the same body. [3]
  • Disabling redirects (supported): Yes—redirect(reqwest::redirect::Policy::none()) is the supported way to disable all redirect behavior. [2], [4]

Sources:
[1] https://docs.rs/reqwest/latest/reqwest/struct.ClientBuilder.html
[2] https://docs.rs/reqwest/latest/reqwest/redirect/
[3] https://docs.rs/reqwest/latest/src/reqwest/redirect.rs.html
[4] https://docs.rs/reqwest/latest/reqwest/redirect/struct.Policy.html


🏁 Script executed:

#!/bin/bash
# Find all references to apply_dns_pinning
rg "apply_dns_pinning" -A 2 -B 2

Repository: parseablehq/parseable

Length of output: 1306


🏁 Script executed:

#!/bin/bash
# Look for other redirect policy usages in the codebase
rg "redirect" -t rs -i

Repository: parseablehq/parseable

Length of output: 91


🏁 Script executed:

#!/bin/bash
# Get broader context around the apply_dns_pinning function
sed -n '600,650p' src/alerts/target.rs

Repository: parseablehq/parseable

Length of output: 1820


🏁 Script executed:

#!/bin/bash
# Find all webhook/callable target implementations that use apply_dns_pinning
rg "impl CallableTarget" -A 10 src/alerts/target.rs | head -100

Repository: parseablehq/parseable

Length of output: 1413


🏁 Script executed:

#!/bin/bash
# Search for redirect-related configuration in the codebase
rg "redirect\(" -i --type-add 'rust:*.rs' --type rust

Repository: parseablehq/parseable

Length of output: 290


Disable HTTP redirects to prevent SSRF via redirect-based attacks.

HTTP redirects bypass DNS pinning. An attacker-controlled webhook can respond with a 30x Location header pointing to an internal address like 169.254.169.254. Reqwest follows redirects by default (including 307/308 responses), allowing this follow-up request to reach internal services unvalidated.

Disable redirects for all webhook clients:

Fix
 async fn apply_dns_pinning(
     mut builder: ClientBuilder,
     endpoint: &Url,
 ) -> Result<ClientBuilder, String> {
+    builder = builder.redirect(reqwest::redirect::Policy::none());
     if let Some((host, addr)) = resolve_and_pin(endpoint).await? {
         builder = builder.resolve(&host, addr);
     }
     Ok(builder)
 }

This affects all webhook implementations that call apply_dns_pinning: SlackWebHook, OtherWebHook, and AlertManager.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/alerts/target.rs` around lines 625 - 633, apply_dns_pinning currently
only sets DNS resolution but leaves reqwest's default redirect behavior enabled,
which allows SSRF via redirect responses; update apply_dns_pinning (which is
used by SlackWebHook, OtherWebHook, and AlertManager) to disable redirects on
the ClientBuilder before returning (set the builder's redirect policy to none
via the reqwest redirect Policy API), so the returned ClientBuilder has
redirects disabled in addition to DNS pinning; keep resolve_and_pin and existing
host-resolution logic unchanged.

Comment on lines +174 to +181
// Validate redirect URL against server host to prevent open redirect attacks
let conn = req.connection_info().clone();
let base_url_without_scheme = format!("{}/", conn.host());
if !is_valid_redirect_url(&base_url_without_scheme, query.redirect.as_str()) {
return Err(OIDCError::BadRequest(
"Bad Request, Invalid Redirect URL!".to_string(),
));
}
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.

⚠️ Potential issue | 🟠 Major

Use the configured external origin for logout redirects.

req.connection_info().host() is request-derived, and is_valid_redirect_url() ignores the scheme, so this still accepts http://<same-host>/ on HTTPS deployments and depends on Host/X-Forwarded-Host being trustworthy. Please validate query.redirect against the configured public base URL instead of the incoming host.

🔒 Suggested direction
-    let conn = req.connection_info().clone();
-    let base_url_without_scheme = format!("{}/", conn.host());
-    if !is_valid_redirect_url(&base_url_without_scheme, query.redirect.as_str()) {
+    let configured = Url::parse(&PARSEABLE.options.address)
+        .map_err(|_| OIDCError::BadRequest("Invalid server base URL".to_string()))?;
+    if query.redirect.scheme() != configured.scheme()
+        || query.redirect.host_str() != configured.host_str()
+        || query.redirect.port_or_known_default() != configured.port_or_known_default()
+    {
         return Err(OIDCError::BadRequest(
             "Bad Request, Invalid Redirect URL!".to_string(),
         ));
     }

Applying the same helper change to login() would close the same gap there.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/handlers/http/oidc.rs` around lines 174 - 181, Replace the
request-derived host check with validation against the configured public base
URL: in the logout handler replace using req.connection_info().host() /
base_url_without_scheme and call is_valid_redirect_url() with the
application-configured external origin/public base URL (the same helper used for
login) when validating query.redirect, returning the same OIDCError::BadRequest
on failure; apply the identical change to login() so both paths validate
redirect URLs against the configured public base URL rather than
Host/X-Forwarded-Host.

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