From c0192ce9b4e2704255e21608f77e9972d744a901 Mon Sep 17 00:00:00 2001 From: Arunesh Dwivedi Date: Tue, 16 Jun 2026 08:29:31 +0000 Subject: [PATCH 01/12] feat: add NAT64-aware SSRF filter wrapper The ssrf-req-filter package blocks NAT64 addresses (64:ff9b::/96) because ipaddr.js classifies them as 'rfc6052' range instead of 'unicast'. This adds a getSsrfFilter wrapper that allows NAT64 addresses. Signed-off-by: Arunesh Dwivedi --- server/Server.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/server/Server.js b/server/Server.js index d6f748a1e..99bcdafed 100644 --- a/server/Server.js +++ b/server/Server.js @@ -87,6 +87,28 @@ class Server { } } global.PodcastDownloadTimeout = toNumber(process.env.PODCAST_DOWNLOAD_TIMEOUT, 30000) + + // SSRF filter wrapper that also allows NAT64 addresses (RFC 6052) + // The ssrf-req-filter package blocks NAT64 addresses (64:ff9b::/96) because + // ipaddr.js classifies them as "rfc6052" range instead of "unicast". + // We need to allow these for environments using NAT64/DNS64. + const ssrfFilterLib = require('ssrf-req-filter') + const net = require('net') + global.getSsrfFilter = (url) => { + const agent = ssrfFilterLib(url) + const originalCreateConnection = agent.createConnection.bind(agent) + agent.createConnection = function(options, callback) { + const { host: address } = options + // Allow NAT64 addresses (64:ff9b::/96 prefix) + if (address && address.startsWith('64:ff9b::')) { + const socket = net.createConnection({ host: options.host, port: options.port }, callback) + return socket + } + return originalCreateConnection(options, callback) + } + return agent + } + global.MaxFailedEpisodeChecks = toNumber(process.env.MAX_FAILED_EPISODE_CHECKS, 24) if (!fs.pathExistsSync(global.ConfigPath)) { From 4d733c339d34e28a2671d10717b573b8083b62ea Mon Sep 17 00:00:00 2001 From: Arunesh Dwivedi Date: Tue, 16 Jun 2026 08:29:53 +0000 Subject: [PATCH 02/12] fix: use getSsrfFilter for NAT64 support in podcastUtils.js Replace ssrfFilter with getSsrfFilter to allow NAT64 addresses (64:ff9b::/96) which are blocked by the default ssrf-req-filter. Signed-off-by: Arunesh Dwivedi --- server/utils/podcastUtils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/utils/podcastUtils.js b/server/utils/podcastUtils.js index 1cb0c4cb4..5ad5ee072 100644 --- a/server/utils/podcastUtils.js +++ b/server/utils/podcastUtils.js @@ -373,8 +373,8 @@ module.exports.getPodcastFeed = (feedUrl, excludeEpisodeMetadata = false) => { 'Accept-Encoding': 'gzip, compress, deflate', 'User-Agent': userAgent }, - httpAgent: global.DisableSsrfRequestFilter?.(feedUrl) ? null : ssrfFilter(feedUrl), - httpsAgent: global.DisableSsrfRequestFilter?.(feedUrl) ? null : ssrfFilter(feedUrl) + httpAgent: global.DisableSsrfRequestFilter?.(feedUrl) ? null : getSsrfFilter(feedUrl), + httpsAgent: global.DisableSsrfRequestFilter?.(feedUrl) ? null : getSsrfFilter(feedUrl) }) .then(async (data) => { // Adding support for ios-8859-1 encoded RSS feeds. From cb40fef974d514fe01941a195ef956b0dfb7bcd4 Mon Sep 17 00:00:00 2001 From: Arunesh Dwivedi Date: Tue, 16 Jun 2026 08:29:55 +0000 Subject: [PATCH 03/12] fix: use getSsrfFilter for NAT64 support in ffmpegHelpers.js Replace ssrfFilter with getSsrfFilter to allow NAT64 addresses (64:ff9b::/96) which are blocked by the default ssrf-req-filter. Signed-off-by: Arunesh Dwivedi --- server/utils/ffmpegHelpers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/utils/ffmpegHelpers.js b/server/utils/ffmpegHelpers.js index 7ad2a3aee..abdfd9703 100644 --- a/server/utils/ffmpegHelpers.js +++ b/server/utils/ffmpegHelpers.js @@ -125,8 +125,8 @@ module.exports.downloadPodcastEpisode = (podcastEpisodeDownload) => { 'User-Agent': userAgent }, timeout: global.PodcastDownloadTimeout, - httpAgent: global.DisableSsrfRequestFilter?.(podcastEpisodeDownload.url) ? null : ssrfFilter(podcastEpisodeDownload.url), - httpsAgent: global.DisableSsrfRequestFilter?.(podcastEpisodeDownload.url) ? null : ssrfFilter(podcastEpisodeDownload.url) + httpAgent: global.DisableSsrfRequestFilter?.(podcastEpisodeDownload.url) ? null : getSsrfFilter(podcastEpisodeDownload.url), + httpsAgent: global.DisableSsrfRequestFilter?.(podcastEpisodeDownload.url) ? null : getSsrfFilter(podcastEpisodeDownload.url) }) Logger.debug(`[ffmpegHelpers] Successfully connected with User-Agent: ${userAgent}`) From 023c3591a48c778131570d74f5d13629561e3a10 Mon Sep 17 00:00:00 2001 From: Arunesh Dwivedi Date: Tue, 16 Jun 2026 08:29:57 +0000 Subject: [PATCH 04/12] fix: use getSsrfFilter for NAT64 support in fileUtils.js Replace ssrfFilter with getSsrfFilter to allow NAT64 addresses (64:ff9b::/96) which are blocked by the default ssrf-req-filter. Signed-off-by: Arunesh Dwivedi --- server/utils/fileUtils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/utils/fileUtils.js b/server/utils/fileUtils.js index 9a349bd54..5952d3a5c 100644 --- a/server/utils/fileUtils.js +++ b/server/utils/fileUtils.js @@ -306,8 +306,8 @@ module.exports.downloadFile = (url, filepath, contentTypeFilter = null) => { 'User-Agent': 'audiobookshelf (+https://audiobookshelf.org)' }, timeout: 30000, - httpAgent: global.DisableSsrfRequestFilter?.(url) ? null : ssrfFilter(url), - httpsAgent: global.DisableSsrfRequestFilter?.(url) ? null : ssrfFilter(url) + httpAgent: global.DisableSsrfRequestFilter?.(url) ? null : getSsrfFilter(url), + httpsAgent: global.DisableSsrfRequestFilter?.(url) ? null : getSsrfFilter(url) }) .then((response) => { // Validate content type From c019810bacc5fa05545fb35df616371836491e0a Mon Sep 17 00:00:00 2001 From: Arunesh Dwivedi Date: Tue, 16 Jun 2026 08:30:34 +0000 Subject: [PATCH 05/12] chore: remove unused ssrf-req-filter import in podcastUtils.js Signed-off-by: Arunesh Dwivedi --- server/utils/podcastUtils.js | 1 - 1 file changed, 1 deletion(-) diff --git a/server/utils/podcastUtils.js b/server/utils/podcastUtils.js index 5ad5ee072..1ea86a3a9 100644 --- a/server/utils/podcastUtils.js +++ b/server/utils/podcastUtils.js @@ -1,5 +1,4 @@ const axios = require('axios') -const ssrfFilter = require('ssrf-req-filter') const Logger = require('../Logger') const { xmlToJSON, timestampToSeconds } = require('./index') const htmlSanitizer = require('../utils/htmlSanitizer') From a742bc9a17616558e27588e6e9965403f7052591 Mon Sep 17 00:00:00 2001 From: Arunesh Dwivedi Date: Tue, 16 Jun 2026 08:30:36 +0000 Subject: [PATCH 06/12] chore: remove unused ssrf-req-filter import in ffmpegHelpers.js Signed-off-by: Arunesh Dwivedi --- server/utils/ffmpegHelpers.js | 1 - 1 file changed, 1 deletion(-) diff --git a/server/utils/ffmpegHelpers.js b/server/utils/ffmpegHelpers.js index abdfd9703..6418eeeff 100644 --- a/server/utils/ffmpegHelpers.js +++ b/server/utils/ffmpegHelpers.js @@ -1,5 +1,4 @@ const axios = require('axios') -const ssrfFilter = require('ssrf-req-filter') const Ffmpeg = require('../libs/fluentFfmpeg') const ffmpgegUtils = require('../libs/fluentFfmpeg/utils') const fs = require('../libs/fsExtra') From d2090d9958a8f186ea03465697311d8612f040f7 Mon Sep 17 00:00:00 2001 From: Arunesh Dwivedi Date: Tue, 16 Jun 2026 08:30:38 +0000 Subject: [PATCH 07/12] chore: remove unused ssrf-req-filter import in fileUtils.js Signed-off-by: Arunesh Dwivedi --- server/utils/fileUtils.js | 1 - 1 file changed, 1 deletion(-) diff --git a/server/utils/fileUtils.js b/server/utils/fileUtils.js index 5952d3a5c..6d06228ab 100644 --- a/server/utils/fileUtils.js +++ b/server/utils/fileUtils.js @@ -1,6 +1,5 @@ const axios = require('axios') const Path = require('path') -const ssrfFilter = require('ssrf-req-filter') const exec = require('child_process').exec const fs = require('../libs/fsExtra') const rra = require('../libs/recursiveReaddirAsync') From 41b25855bf7a9883bbdf24636cacdfe06083dfcd Mon Sep 17 00:00:00 2001 From: Arunesh Dwivedi Date: Tue, 16 Jun 2026 09:34:38 +0000 Subject: [PATCH 08/12] fix: use global.getSsrfFilter in ffmpegHelpers.js The getSsrfFilter function is set on global in Server.js, so other modules must access it via global.getSsrfFilter(). Signed-off-by: Arunesh Dwivedi --- server/utils/ffmpegHelpers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/utils/ffmpegHelpers.js b/server/utils/ffmpegHelpers.js index 6418eeeff..6976134e7 100644 --- a/server/utils/ffmpegHelpers.js +++ b/server/utils/ffmpegHelpers.js @@ -124,8 +124,8 @@ module.exports.downloadPodcastEpisode = (podcastEpisodeDownload) => { 'User-Agent': userAgent }, timeout: global.PodcastDownloadTimeout, - httpAgent: global.DisableSsrfRequestFilter?.(podcastEpisodeDownload.url) ? null : getSsrfFilter(podcastEpisodeDownload.url), - httpsAgent: global.DisableSsrfRequestFilter?.(podcastEpisodeDownload.url) ? null : getSsrfFilter(podcastEpisodeDownload.url) + httpAgent: global.DisableSsrfRequestFilter?.(podcastEpisodeDownload.url) ? null : global.getSsrfFilter(podcastEpisodeDownload.url), + httpsAgent: global.DisableSsrfRequestFilter?.(podcastEpisodeDownload.url) ? null : global.getSsrfFilter(podcastEpisodeDownload.url) }) Logger.debug(`[ffmpegHelpers] Successfully connected with User-Agent: ${userAgent}`) From 32955b897e116281bea779f335f1477e15051308 Mon Sep 17 00:00:00 2001 From: Arunesh Dwivedi Date: Tue, 16 Jun 2026 09:34:40 +0000 Subject: [PATCH 09/12] fix: use global.getSsrfFilter in fileUtils.js The getSsrfFilter function is set on global in Server.js, so other modules must access it via global.getSsrfFilter(). Signed-off-by: Arunesh Dwivedi --- server/utils/fileUtils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/utils/fileUtils.js b/server/utils/fileUtils.js index 6d06228ab..95c6ddd27 100644 --- a/server/utils/fileUtils.js +++ b/server/utils/fileUtils.js @@ -305,8 +305,8 @@ module.exports.downloadFile = (url, filepath, contentTypeFilter = null) => { 'User-Agent': 'audiobookshelf (+https://audiobookshelf.org)' }, timeout: 30000, - httpAgent: global.DisableSsrfRequestFilter?.(url) ? null : getSsrfFilter(url), - httpsAgent: global.DisableSsrfRequestFilter?.(url) ? null : getSsrfFilter(url) + httpAgent: global.DisableSsrfRequestFilter?.(url) ? null : global.getSsrfFilter(url), + httpsAgent: global.DisableSsrfRequestFilter?.(url) ? null : global.getSsrfFilter(url) }) .then((response) => { // Validate content type From dcf809c138987ef3a9b6b50b97689da093f23a4a Mon Sep 17 00:00:00 2001 From: Arunesh Dwivedi Date: Tue, 16 Jun 2026 09:34:42 +0000 Subject: [PATCH 10/12] fix: use global.getSsrfFilter in podcastUtils.js The getSsrfFilter function is set on global in Server.js, so other modules must access it via global.getSsrfFilter(). Signed-off-by: Arunesh Dwivedi --- server/utils/podcastUtils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/utils/podcastUtils.js b/server/utils/podcastUtils.js index 1ea86a3a9..1cff93266 100644 --- a/server/utils/podcastUtils.js +++ b/server/utils/podcastUtils.js @@ -372,8 +372,8 @@ module.exports.getPodcastFeed = (feedUrl, excludeEpisodeMetadata = false) => { 'Accept-Encoding': 'gzip, compress, deflate', 'User-Agent': userAgent }, - httpAgent: global.DisableSsrfRequestFilter?.(feedUrl) ? null : getSsrfFilter(feedUrl), - httpsAgent: global.DisableSsrfRequestFilter?.(feedUrl) ? null : getSsrfFilter(feedUrl) + httpAgent: global.DisableSsrfRequestFilter?.(feedUrl) ? null : global.getSsrfFilter(feedUrl), + httpsAgent: global.DisableSsrfRequestFilter?.(feedUrl) ? null : global.getSsrfFilter(feedUrl) }) .then(async (data) => { // Adding support for ios-8859-1 encoded RSS feeds. From 53c1e851ab7052c1c5a74b4d616223494f203239 Mon Sep 17 00:00:00 2001 From: Arunesh Dwivedi Date: Tue, 16 Jun 2026 09:38:19 +0000 Subject: [PATCH 11/12] fix: validate embedded IPv4 in NAT64 addresses NAT64 addresses (64:ff9b::/96) embed an IPv4 in the last 32 bits. Before bypassing SSRF, extract and validate the embedded IPv4 is not private/reserved. Signed-off-by: Arunesh Dwivedi --- server/Server.js | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/server/Server.js b/server/Server.js index 99bcdafed..3a5cccb11 100644 --- a/server/Server.js +++ b/server/Server.js @@ -91,18 +91,32 @@ class Server { // SSRF filter wrapper that also allows NAT64 addresses (RFC 6052) // The ssrf-req-filter package blocks NAT64 addresses (64:ff9b::/96) because // ipaddr.js classifies them as "rfc6052" range instead of "unicast". - // We need to allow these for environments using NAT64/DNS64. + // We allow NAT64 but still validate the embedded IPv4 is not private. const ssrfFilterLib = require('ssrf-req-filter') const net = require('net') + const ipaddr = require('ipaddr.js') global.getSsrfFilter = (url) => { const agent = ssrfFilterLib(url) const originalCreateConnection = agent.createConnection.bind(agent) agent.createConnection = function(options, callback) { const { host: address } = options - // Allow NAT64 addresses (64:ff9b::/96 prefix) if (address && address.startsWith('64:ff9b::')) { - const socket = net.createConnection({ host: options.host, port: options.port }, callback) - return socket + try { + const addr = ipaddr.parse(address) + if (addr.kind() === 'ipv6') { + const parts = addr.toNormalizedString().split(':') + const lastPart = parts[parts.length - 1] + if (lastPart && lastPart.includes('.')) { + const ipv4Addr = ipaddr.parse(lastPart) + if (ipv4Addr.range() !== 'unicast') { + throw new Error('NAT64 embeds non-unicast IPv4') + } + } + } + } catch (e) { + throw new Error(`Call to ${address} is blocked: ${e.message}`) + } + return net.createConnection({ host: options.host, port: options.port }, callback) } return originalCreateConnection(options, callback) } From cdae28c84876aee6b978cc8c39bdd93181f85865 Mon Sep 17 00:00:00 2001 From: Arunesh Dwivedi Date: Tue, 16 Jun 2026 09:39:51 +0000 Subject: [PATCH 12/12] fix: correct NAT64 embedded IPv4 extraction NAT64 addresses encode the IPv4 in the last 32 bits as two 16-bit hex groups, not dotted-decimal. Extract properly and validate the embedded IPv4 is not private. Signed-off-by: Arunesh Dwivedi --- server/Server.js | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/server/Server.js b/server/Server.js index 3a5cccb11..0b18af3b2 100644 --- a/server/Server.js +++ b/server/Server.js @@ -95,21 +95,38 @@ class Server { const ssrfFilterLib = require('ssrf-req-filter') const net = require('net') const ipaddr = require('ipaddr.js') + // Check if an IPv4 address string is private/reserved + function isPrivateIPv4(ip) { + try { + const addr = ipaddr.IPv4.parse(ip) + return addr.range() !== 'unicast' + } catch { + return false + } + } global.getSsrfFilter = (url) => { const agent = ssrfFilterLib(url) const originalCreateConnection = agent.createConnection.bind(agent) agent.createConnection = function(options, callback) { const { host: address } = options + // Allow NAT64 addresses (64:ff9b::/96 prefix) but validate embedded IPv4 if (address && address.startsWith('64:ff9b::')) { try { const addr = ipaddr.parse(address) if (addr.kind() === 'ipv6') { + // Extract the embedded IPv4 from the last 32 bits of the NAT64 address + // NAT64 format: 64:ff9b::[96-bit prefix][32-bit IPv4] + // The IPv4 is encoded as two 16-bit hex groups in the last two positions const parts = addr.toNormalizedString().split(':') - const lastPart = parts[parts.length - 1] - if (lastPart && lastPart.includes('.')) { - const ipv4Addr = ipaddr.parse(lastPart) - if (ipv4Addr.range() !== 'unicast') { - throw new Error('NAT64 embeds non-unicast IPv4') + if (parts.length >= 2) { + // Last two 16-bit groups contain the 32-bit IPv4 + const high = parseInt(parts[parts.length - 2], 16) + const low = parseInt(parts[parts.length - 1], 16) + if (!isNaN(high) && !isNaN(low)) { + const ipv4 = `${(high >> 8) & 0xff}.${high & 0xff}.${(low >> 8) & 0xff}.${low & 0xff}` + if (isPrivateIPv4(ipv4)) { + throw new Error(`NAT64 address embeds private IPv4: ${ipv4}`) + } } } }