fix: support query parameter-based secret validation fallback to fix Ombi webhooks (#47)
Build and Push Docker Image / build (push) Successful in 2m4s
Docs Check / Markdown lint (push) Successful in 2m7s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 3m3s
CI / Security audit (push) Successful in 3m42s
Docs Check / Mermaid diagram parse check (push) Failing after 3m58s
CI / Tests & coverage (push) Successful in 4m21s
CI / Swagger Validation & Coverage (push) Successful in 4m33s

This commit is contained in:
2026-05-24 21:25:38 +01:00
parent b5b4862e15
commit 7b9c895888
10 changed files with 144 additions and 25 deletions
+25 -4
View File
@@ -22,7 +22,7 @@ info:
## SSE Streaming
Real-time updates are available via Server-Sent Events at GET /api/dashboard/stream.
version: 1.7.14
version: 1.7.15
contact:
name: sofarr
license:
@@ -795,8 +795,15 @@ paths:
post:
tags: [Webhook]
summary: Sonarr webhook
description: Receives webhook events from Sonarr. Requires X-Sofarr-Webhook-Secret header.
description: Receives webhook events from Sonarr. Requires X-Sofarr-Webhook-Secret header or secret query parameter.
security: []
parameters:
- name: secret
in: query
required: false
schema:
type: string
description: Webhook secret token (alternative to X-Sofarr-Webhook-Secret header)
requestBody:
required: true
content:
@@ -832,8 +839,15 @@ paths:
post:
tags: [Webhook]
summary: Radarr webhook
description: Receives webhook events from Radarr. Requires X-Sofarr-Webhook-Secret header.
description: Receives webhook events from Radarr. Requires X-Sofarr-Webhook-Secret header or secret query parameter.
security: []
parameters:
- name: secret
in: query
required: false
schema:
type: string
description: Webhook secret token (alternative to X-Sofarr-Webhook-Secret header)
requestBody:
required: true
content:
@@ -869,8 +883,15 @@ paths:
post:
tags: [Webhook]
summary: Ombi webhook
description: Receives webhook events from Ombi. Requires X-Sofarr-Webhook-Secret header.
description: Receives webhook events from Ombi. Requires X-Sofarr-Webhook-Secret header or secret query parameter.
security: []
parameters:
- name: secret
in: query
required: false
schema:
type: string
description: Webhook secret token (alternative to X-Sofarr-Webhook-Secret header)
requestBody:
required: true
content:
+1 -1
View File
@@ -221,7 +221,7 @@ router.post('/webhook/enable', requireAuth, async (req, res) => {
}
const ombiInst = ombiInstances[0];
const webhookUrl = `${sofarrBaseUrl}/api/webhook/ombi`;
const webhookUrl = `${sofarrBaseUrl}/api/webhook/ombi?secret=${webhookSecret}`;
// Call Ombi API to register webhook
const axios = require('axios');
+31 -11
View File
@@ -144,13 +144,13 @@ const OMBI_EVENTS = new Set([
]);
/**
* Validate webhook secret from the X-Sofarr-Webhook-Secret header
* Validate webhook secret from the X-Sofarr-Webhook-Secret header or secret query parameter
* @param {Object} req - Express request object
* @returns {boolean} True if secret is valid, false otherwise
*/
function validateWebhookSecret(req) {
const expectedSecret = getWebhookSecret();
const providedSecret = req.get('X-Sofarr-Webhook-Secret');
const providedSecret = req.get('X-Sofarr-Webhook-Secret') || req.query.secret;
if (!expectedSecret) {
logToFile('[Webhook] WARNING: SOFARR_WEBHOOK_SECRET not configured, rejecting webhook');
@@ -158,7 +158,7 @@ function validateWebhookSecret(req) {
}
if (!providedSecret) {
logToFile('[Webhook] WARNING: Missing X-Sofarr-Webhook-Secret header');
logToFile('[Webhook] WARNING: Missing X-Sofarr-Webhook-Secret header or secret query parameter');
return false;
}
@@ -309,13 +309,13 @@ function validatePayload(body) {
* Receives webhook events from Sonarr instances. Validates the secret, logs the event,
* refreshes cache, broadcasts SSE, and returns 200 immediately (fire-and-forget processing).
*
* **Authentication:** Requires `X-Sofarr-Webhook-Secret` header matching `SOFARR_WEBHOOK_SECRET`.
* **Authentication:** Requires `X-Sofarr-Webhook-Secret` header or `secret` query parameter matching `SOFARR_WEBHOOK_SECRET`.
* No cookie authentication required (webhooks come from Sonarr, not browsers).
*
* **Rate Limiting:** 60 requests per minute per IP.
*
* **Validation:**
* - Secret validation via `X-Sofarr-Webhook-Secret` header
* - Secret validation via `X-Sofarr-Webhook-Secret` header or `secret` query parameter
* - Payload validation (must be JSON object with eventType, instanceName, date)
* - Event type must be in allowlist (Test, Grab, Download, DownloadFailed, etc.)
* - Replay protection: rejects duplicate events within 5-minute window
@@ -342,6 +342,13 @@ function validatePayload(body) {
* - Header: `X-Sofarr-Webhook-Secret: {SOFARR_WEBHOOK_SECRET}`
* - Events: onGrab, onDownload, onUpgrade, onImport
* security: []
* parameters:
* - name: secret
* in: query
* required: false
* schema:
* type: string
* description: Webhook secret token (alternative to X-Sofarr-Webhook-Secret header)
* requestBody:
* required: true
* content:
@@ -456,13 +463,13 @@ router.post('/sonarr', webhookLimiter, (req, res) => {
* Receives webhook events from Radarr instances. Validates the secret, logs the event,
* refreshes cache, broadcasts SSE, and returns 200 immediately (fire-and-forget processing).
*
* **Authentication:** Requires `X-Sofarr-Webhook-Secret` header matching `SOFARR_WEBHOOK_SECRET`.
* **Authentication:** Requires `X-Sofarr-Webhook-Secret` header or `secret` query parameter matching `SOFARR_WEBHOOK_SECRET`.
* No cookie authentication required (webhooks come from Radarr, not browsers).
*
* **Rate Limiting:** 60 requests per minute per IP.
*
* **Validation:**
* - Secret validation via `X-Sofarr-Webhook-Secret` header
* - Secret validation via `X-Sofarr-Webhook-Secret` header or `secret` query parameter
* - Payload validation (must be JSON object with eventType, instanceName, date)
* - Event type must be in allowlist (Test, Grab, Download, DownloadFailed, etc.)
* - Replay protection: rejects duplicate events within 5-minute window
@@ -489,6 +496,13 @@ router.post('/sonarr', webhookLimiter, (req, res) => {
* - Header: `X-Sofarr-Webhook-Secret: {SOFARR_WEBHOOK_SECRET}`
* - Events: onGrab, onDownload, onUpgrade, onImport
* security: []
* parameters:
* - name: secret
* in: query
* required: false
* schema:
* type: string
* description: Webhook secret token (alternative to X-Sofarr-Webhook-Secret header)
* requestBody:
* required: true
* content:
@@ -603,13 +617,13 @@ router.post('/radarr', webhookLimiter, (req, res) => {
* Receives webhook events from Ombi instances. Validates the secret, logs the event,
* refreshes cache, broadcasts SSE, and returns 200 immediately (fire-and-forget processing).
*
* **Authentication:** Requires `X-Sofarr-Webhook-Secret` header matching `SOFARR_WEBHOOK_SECRET`.
* **Authentication:** Requires `X-Sofarr-Webhook-Secret` header or `secret` query parameter matching `SOFARR_WEBHOOK_SECRET`.
* No cookie authentication required (webhooks come from Ombi, not browsers).
*
* **Rate Limiting:** 60 requests per minute per IP.
*
* **Validation:**
* - Secret validation via `X-Sofarr-Webhook-Secret` header
* - Secret validation via `X-Sofarr-Webhook-Secret` header or `secret` query parameter
* - Payload validation (must be JSON object with notificationType, requestId)
* - Event type must be in allowlist (RequestAvailable, RequestApproved, RequestDeclined, RequestPending, RequestProcessing)
* - Replay protection: rejects duplicate events within 5-minute window
@@ -627,11 +641,17 @@ router.post('/radarr', webhookLimiter, (req, res) => {
* 6. Background: fetch fresh data from Ombi, update cache, broadcast SSE
*
* **x-integration-notes:** Configure Ombi webhook:
* - URL: `{SOFARR_BASE_URL}/api/webhook/ombi`
* - URL: `{SOFARR_BASE_URL}/api/webhook/ombi?secret={SOFARR_WEBHOOK_SECRET}`
* - Method: POST
* - Header: `X-Sofarr-Webhook-Secret: {SOFARR_WEBHOOK_SECRET}`
* - Application Token: OMBI_API_KEY
* security: []
* parameters:
* - name: secret
* in: query
* required: false
* schema:
* type: string
* description: Webhook secret token (alternative to X-Sofarr-Webhook-Secret header)
* requestBody:
* required: true
* content: