CVE-2026-77067 Overview
CVE-2026-77067 is a stored Server-Side Request Forgery (SSRF) vulnerability in the Omnivore read-it-later application. The setWebhookResolver function in packages/api/src/resolvers/webhooks/index.ts accepts a caller-supplied URL and stores it without validating the destination address. When a subscribed event fires, callWebhook in packages/api/src/jobs/call_webhook.ts issues axios.request against the stored URL with attacker-controlled method, Content-Type, and JSON body. An authenticated user can direct the server to send repeated requests to internal endpoints, including link-local cloud metadata addresses such as 169.254.169.254. The weakness is classified as [CWE-918].
Critical Impact
Authenticated users can coerce the Omnivore API server into issuing blind HTTP requests to internal network resources and cloud metadata services.
Affected Products
- Omnivore read-it-later application (omnivore-app/omnivore)
- packages/api webhook resolver component
- Deployments running commit 0d66408 and earlier without the c4d7d85 fix
Discovery Timeline
- 2026-08-20 - CVE-2026-77067 published to NVD
- 2026-08-20 - Last updated in NVD database
Technical Details for CVE-2026-77067
Vulnerability Analysis
The Omnivore API exposes a GraphQL setWebhook mutation that persists webhook definitions per user. The resolver in packages/api/src/resolvers/webhooks/index.ts writes the supplied url, HTTP method, and Content-Type fields directly to storage. The file imports no validation helper, so private, loopback, and link-local addresses are accepted alongside public destinations.
When a triggering event fires, the job runner in packages/api/src/jobs/call_webhook.ts calls axios.request using the stored parameters and a JSON body containing the event payload. The response is discarded. Only a success line or the raw axios error is written to the server log, making this a blind SSRF from the attacker's perspective. Data exfiltration therefore depends on secondary channels such as log access or side effects at the target.
Because the request originates from the API server, attacker-shaped traffic reaches services that trust the server's network position, including cloud instance metadata endpoints, internal admin panels, and unauthenticated microservices bound to loopback.
Root Cause
The root cause is missing destination-URL validation in the webhook resolver. The pre-patch code path never checked the host against a deny list of private or link-local ranges and never imported a validation helper. The validateUrl function already existed in packages/api/src/services/create_page_save_request but was not applied to webhook URLs.
Attack Vector
An authenticated Omnivore user issues a setWebhook mutation with a url pointing at an internal target, for example http://169.254.169.254/latest/meta-data/iam/security-credentials/. When the subscribed event fires, the server executes axios.request against that URL with attacker-selected method and Content-Type. The attacker controls request shape but cannot read the response through the API.
// Security patch applied in packages/api/src/resolvers/webhooks/index.ts
import { deleteWebhook } from '../../services/webhook'
import { analytics } from '../../utils/analytics'
import { authorized } from '../../utils/gql-utils'
+import { validateUrl } from '../../services/create_page_save_request'
export const webhooksResolver = authorized<WebhooksSuccess, WebhooksError>(
async (_obj, _params, { uid, log }) => {
// Source: https://github.com/omnivore-app/omnivore/commit/c4d7d8562e6b9aabb1d8e4dabca268e314baa43a
Detection Methods for CVE-2026-77067
Indicators of Compromise
- Outbound HTTP requests from the Omnivore API host to RFC1918 ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) or link-local 169.254.0.0/16.
- Server log entries from call_webhook recording success or axios errors for internal hostnames or IPs.
- setWebhook GraphQL mutations whose stored url field resolves to non-public addresses.
- Repeated webhook fires against the same internal endpoint following a subscription creation.
Detection Strategies
- Inspect the webhooks table for rows whose url column resolves to private, loopback, or link-local addresses and correlate with the creating uid.
- Parse API server logs for call_webhook entries and alert on any destination outside an allowed egress list.
- Enable egress network flow logging on the API container and alert on connections to 169.254.169.254 or internal service CIDRs.
Monitoring Recommendations
- Forward API server logs and network flow logs to a centralized analytics platform for cross-correlation between GraphQL activity and outbound requests.
- Baseline expected webhook destinations per tenant and flag deviations, especially first-seen internal IPs.
- Monitor cloud provider metadata service access logs where available, such as IMDSv2 token issuance patterns on AWS.
How to Mitigate CVE-2026-77067
Immediate Actions Required
- Apply the upstream fix from commit c4d7d85 which adds validateUrl to the webhook and scan-feeds resolvers.
- Audit the webhooks table and delete any rows whose URL targets private, loopback, or link-local addresses.
- Rotate cloud instance credentials if IMDS access from the API host cannot be ruled out.
- Enforce IMDSv2 with hop-limit 1 on AWS deployments to blunt metadata SSRF.
Patch Information
The fix is delivered in commit c4d7d8562e6b9aabb1d8e4dabca268e314baa43a titled "fix(): fix ssrf via scan feeds and webhooks (#4656)". The patch imports validateUrl from services/create_page_save_request and invokes it inside both the webhook resolver and the scan-feeds resolver before any outbound HTTP request. See the VulnCheck Omnivore Advisory and GitHub Issue #4647 for background.
Workarounds
- Place the Omnivore API behind an egress proxy that denies RFC1918 and 169.254.0.0/16 destinations.
- Run the API container in a network namespace with no route to the cloud metadata service.
- Restrict the setWebhook mutation to trusted operator accounts until the patch is deployed.
# Example egress deny rules for the Omnivore API host
iptables -A OUTPUT -d 169.254.169.254 -j REJECT
iptables -A OUTPUT -d 10.0.0.0/8 -j REJECT
iptables -A OUTPUT -d 172.16.0.0/12 -j REJECT
iptables -A OUTPUT -d 192.168.0.0/16 -j REJECT
iptables -A OUTPUT -d 127.0.0.0/8 ! -o lo -j REJECT
Disclaimer: This content was generated using AI. While we strive for accuracy, please verify critical information with official sources.

