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
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:
+47
-7
@@ -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'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user