fix: secure webhook config endpoint and validate config on Ombi enable/test
Build and Push Docker Image / build (push) Successful in 1m4s
Docs Check / Markdown lint (push) Successful in 1m49s
Licence Check / Licence compatibility and copyright header verification (push) Failing after 2m22s
CI / Security audit (push) Successful in 2m44s
CI / Swagger Validation & Coverage (push) Successful in 2m59s
Docs Check / Mermaid diagram parse check (push) Successful in 3m11s
CI / Tests & coverage (push) Successful in 3m27s

- Add requireAuth to GET /api/webhook/config to enforce authentication
- Add SOFARR_BASE_URL and SOFARR_WEBHOOK_SECRET validation to POST /api/ombi/webhook/enable and /test
- Return 400 with descriptive errors when webhook config is missing on Ombi routes
- Clean up test environment in webhook.test.js afterEach
- Add regression tests for all new validation logic
- Update CHANGELOG.md with security fixes
This commit is contained in:
2026-05-22 09:50:30 +01:00
parent f1e0a77fad
commit dbf45ec31d
6 changed files with 392 additions and 22 deletions
+47 -7
View File
@@ -2,7 +2,7 @@
const express = require('express');
const { logToFile } = require('../utils/logger');
const cache = require('../utils/cache');
const { getOmbiInstances } = require('../utils/config');
const { getOmbiInstances, getWebhookSecret, getSofarrBaseUrl } = require('../utils/config');
const requireAuth = require('../middleware/requireAuth');
const { extractRequestedUser } = require('../utils/ombiHelpers');
@@ -153,13 +153,23 @@ router.get('/requests', requireAuth, async (req, res) => {
*/
router.post('/webhook/enable', requireAuth, async (req, res) => {
try {
const sofarrBaseUrl = getSofarrBaseUrl();
const webhookSecret = getWebhookSecret();
if (!sofarrBaseUrl) {
return res.status(400).json({ error: 'SOFARR_BASE_URL not configured' });
}
if (!webhookSecret) {
return res.status(400).json({ error: 'SOFARR_WEBHOOK_SECRET not configured' });
}
const ombiInstances = getOmbiInstances();
if (ombiInstances.length === 0) {
return res.status(400).json({ error: 'Ombi not configured' });
}
const ombiInst = ombiInstances[0];
const webhookUrl = `${process.env.SOFARR_BASE_URL}/api/webhook/ombi`;
const webhookUrl = `${sofarrBaseUrl}/api/webhook/ombi`;
// Call Ombi API to register webhook
const axios = require('axios');
@@ -261,11 +271,31 @@ router.post('/webhook/enable', requireAuth, async (req, res) => {
*/
router.get('/webhook/status', requireAuth, async (req, res) => {
try {
const sofarrBaseUrl = getSofarrBaseUrl();
const webhookSecret = getWebhookSecret();
// Webhooks require SOFARR_BASE_URL and SOFARR_WEBHOOK_SECRET to be configured
if (!sofarrBaseUrl || !webhookSecret) {
return res.json({
enabled: false,
webhookUrl: null,
applicationToken: null,
triggers: {
requestAvailable: false,
requestApproved: false,
requestDeclined: false,
requestPending: false,
requestProcessing: false
},
stats: null
});
}
const ombiInstances = getOmbiInstances();
if (ombiInstances.length === 0) {
return res.json({
enabled: false,
webhookUrl: null,
return res.json({
enabled: false,
webhookUrl: null,
applicationToken: null,
triggers: {
requestAvailable: false,
@@ -360,13 +390,23 @@ router.get('/webhook/status', requireAuth, async (req, res) => {
*/
router.post('/webhook/test', requireAuth, async (req, res) => {
try {
const sofarrBaseUrl = getSofarrBaseUrl();
const webhookSecret = getWebhookSecret();
if (!sofarrBaseUrl) {
return res.status(400).json({ error: 'SOFARR_BASE_URL not configured' });
}
if (!webhookSecret) {
return res.status(400).json({ error: 'SOFARR_WEBHOOK_SECRET not configured' });
}
const ombiInstances = getOmbiInstances();
if (ombiInstances.length === 0) {
return res.status(400).json({ error: 'Ombi not configured' });
}
const ombiInst = ombiInstances[0];
const webhookUrl = `${process.env.SOFARR_BASE_URL}/api/webhook/ombi`;
const webhookUrl = `${sofarrBaseUrl}/api/webhook/ombi`;
// Simulate a test webhook event
const axios = require('axios');
@@ -379,7 +419,7 @@ router.post('/webhook/test', requireAuth, async (req, res) => {
requestStatus: 'Pending'
}, {
headers: {
'X-Sofarr-Webhook-Secret': process.env.SOFARR_WEBHOOK_SECRET,
'X-Sofarr-Webhook-Secret': webhookSecret,
'Content-Type': 'application/json'
}
});
+58 -1
View File
@@ -2,14 +2,71 @@
const express = require('express');
const rateLimit = require('express-rate-limit');
const { logToFile } = require('../utils/logger');
const { getWebhookSecret, getSonarrInstances, getRadarrInstances, getOmbiInstances } = require('../utils/config');
const { getWebhookSecret, getSonarrInstances, getRadarrInstances, getOmbiInstances, getSofarrBaseUrl } = require('../utils/config');
const cache = require('../utils/cache');
const arrRetrieverRegistry = require('../utils/arrRetrievers');
const { pollAllServices, POLL_INTERVAL, POLLING_ENABLED } = require('../utils/poller');
const { extractRequestedUser } = require('../utils/ombiHelpers');
const requireAuth = require('../middleware/requireAuth');
const router = express.Router();
/**
* @openapi
* /api/webhook/config:
* get:
* tags: [Webhook]
* summary: Get webhook configuration status
* description: |
* Returns whether the required webhook configuration (SOFARR_BASE_URL and SOFARR_WEBHOOK_SECRET)
* is properly configured. Used by the webhooks panel to determine if webhooks can be enabled.
*
* **Authentication:** Requires valid `emby_user` cookie.
* security:
* - CookieAuth: []
* responses:
* '200':
* description: Webhook configuration status
* content:
* application/json:
* schema:
* type: object
* properties:
* valid:
* type: boolean
* description: true if both SOFARR_BASE_URL and SOFARR_WEBHOOK_SECRET are configured
* example: true
* missing:
* type: array
* items:
* type: string
* description: List of missing configuration items
* example: []
* '401':
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
router.get('/config', requireAuth, (req, res) => {
const sofarrBaseUrl = getSofarrBaseUrl();
const webhookSecret = getWebhookSecret();
const missing = [];
if (!sofarrBaseUrl) {
missing.push('SOFARR_BASE_URL');
}
if (!webhookSecret) {
missing.push('SOFARR_WEBHOOK_SECRET');
}
res.json({
valid: missing.length === 0,
missing
});
});
// Dedicated rate limiter for webhook endpoints — stricter than the global API limiter.
// Sonarr/Radarr send at most one event per action; 60/min per IP is generous.
// In tests, SKIP_RATE_LIMIT=1 raises the ceiling to effectively unlimited.