From 44cff97a2ed1e7240e48939730972d050f7f55be Mon Sep 17 00:00:00 2001 From: mononaut Date: Thu, 28 May 2026 09:59:15 +0000 Subject: [PATCH] improve services backend proxy validation and error handling --- backend/src/api/about.routes.ts | 34 +++++++++++++++---- .../api/acceleration/acceleration.routes.ts | 20 ++++++----- backend/src/api/liquid/liquid.routes.ts | 9 ++++- 3 files changed, 47 insertions(+), 16 deletions(-) diff --git a/backend/src/api/about.routes.ts b/backend/src/api/about.routes.ts index 8ea052d12..f8bd0f2bc 100644 --- a/backend/src/api/about.routes.ts +++ b/backend/src/api/about.routes.ts @@ -3,6 +3,8 @@ import config from '../config'; import axios from 'axios'; import logger from '../logger'; +const PROXY_PATH_SEGMENT_REGEX = /^(?!\.{1,2}$)[^\p{Cc}/?#\\]{1,256}$/u; + class AboutRoutes { public initRoutes(app: Application) { app @@ -15,8 +17,13 @@ class AboutRoutes { } }) .get(config.MEMPOOL.API_URL_PREFIX + 'donations/images/:id', async (req, res) => { + if (!PROXY_PATH_SEGMENT_REGEX.test(req.params.id)) { + res.status(400).end(); + return; + } + try { - const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/donations/images/${req.params.id}`, { + const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/donations/images/${encodeURIComponent(req.params.id)}`, { responseType: 'stream', timeout: 10000 }); response.data.pipe(res); @@ -33,8 +40,13 @@ class AboutRoutes { } }) .get(config.MEMPOOL.API_URL_PREFIX + 'contributors/images/:id', async (req, res) => { + if (!PROXY_PATH_SEGMENT_REGEX.test(req.params.id)) { + res.status(400).end(); + return; + } + try { - const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/contributors/images/${req.params.id}`, { + const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/contributors/images/${encodeURIComponent(req.params.id)}`, { responseType: 'stream', timeout: 10000 }); response.data.pipe(res); @@ -51,8 +63,13 @@ class AboutRoutes { } }) .get(config.MEMPOOL.API_URL_PREFIX + 'translators/images/:id', async (req, res) => { + if (!PROXY_PATH_SEGMENT_REGEX.test(req.params.id)) { + res.status(400).end(); + return; + } + try { - const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators/images/${req.params.id}`, { + const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators/images/${encodeURIComponent(req.params.id)}`, { responseType: 'stream', timeout: 10000 }); response.data.pipe(res); @@ -61,7 +78,7 @@ class AboutRoutes { } }) .get(config.MEMPOOL.API_URL_PREFIX + 'services/sponsors', async (req, res) => { - const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`; + const url = `${config.MEMPOOL_SERVICES.API}/sponsors`; try { const response = await axios.get(url, { responseType: 'stream', timeout: 10000 }); response.data.pipe(res); @@ -71,7 +88,12 @@ class AboutRoutes { } }) .get(config.MEMPOOL.API_URL_PREFIX + 'services/account/images/:username/:md5', async (req, res) => { - const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`; + if (!PROXY_PATH_SEGMENT_REGEX.test(req.params.username) || !PROXY_PATH_SEGMENT_REGEX.test(req.params.md5)) { + res.status(400).end(); + return; + } + + const url = `${config.MEMPOOL_SERVICES.API}/account/images/${encodeURIComponent(req.params.username)}/${encodeURIComponent(req.params.md5)}`; try { const response = await axios.get(url, { responseType: 'stream', timeout: 10000 }); response.data.pipe(res); @@ -84,4 +106,4 @@ class AboutRoutes { } } -export default new AboutRoutes(); \ No newline at end of file +export default new AboutRoutes(); diff --git a/backend/src/api/acceleration/acceleration.routes.ts b/backend/src/api/acceleration/acceleration.routes.ts index 8be767287..372d1292c 100644 --- a/backend/src/api/acceleration/acceleration.routes.ts +++ b/backend/src/api/acceleration/acceleration.routes.ts @@ -5,16 +5,18 @@ import logger from '../../logger'; import mempool from '../mempool'; import AccelerationRepository from '../../repositories/AccelerationRepository'; +const TXID_REGEX = /^[a-f0-9]{64}$/i; + class AccelerationRoutes { private tag = 'Accelerator'; public initRoutes(app: Application): void { app .get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations', this.$getAcceleratorAccelerations.bind(this)) - .get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/:txid', this.$getAcceleratorAcceleration.bind(this)) .get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/history', this.$getAcceleratorAccelerationsHistory.bind(this)) .get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/history/aggregated', this.$getAcceleratorAccelerationsHistoryAggregated.bind(this)) .get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/stats', this.$getAcceleratorAccelerationsStats.bind(this)) + .get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/:txid', this.$getAcceleratorAcceleration.bind(this)) .post(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/estimate', this.$getAcceleratorEstimate.bind(this)) ; } @@ -26,7 +28,7 @@ class AccelerationRoutes { /** @asyncUnsafe */ private async $getAcceleratorAcceleration(req: Request, res: Response): Promise { - if (req.params.txid) { + if (req.params.txid && TXID_REGEX.test(req.params.txid)) { const acceleration = await AccelerationRepository.$getAccelerationInfoForTxid(req.params.txid); if (acceleration) { res.status(200).send(acceleration); @@ -34,7 +36,7 @@ class AccelerationRoutes { res.status(404).send('Acceleration not found'); } } else { - res.status(400).send('txid is required'); + res.status(400).send('invalid txid'); } } @@ -55,9 +57,9 @@ class AccelerationRoutes { } private async $getAcceleratorAccelerationsHistoryAggregated(req: Request, res: Response): Promise { - const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`; + const url = `${config.MEMPOOL_SERVICES.API}/accelerator/accelerations/history/aggregated`; try { - const response = await axios.get(url, { responseType: 'stream', timeout: 10000 }); + const response = await axios.get(url, { params: req.query, responseType: 'stream', timeout: 10000 }); for (const key in response.headers) { res.setHeader(key, response.headers[key]); } @@ -69,9 +71,9 @@ class AccelerationRoutes { } private async $getAcceleratorAccelerationsStats(req: Request, res: Response): Promise { - const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`; + const url = `${config.MEMPOOL_SERVICES.API}/accelerator/accelerations/stats`; try { - const response = await axios.get(url, { responseType: 'stream', timeout: 10000 }); + const response = await axios.get(url, { params: req.query, responseType: 'stream', timeout: 10000 }); for (const key in response.headers) { res.setHeader(key, response.headers[key]); } @@ -83,7 +85,7 @@ class AccelerationRoutes { } private async $getAcceleratorEstimate(req: Request, res: Response): Promise { - const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`; + const url = `${config.MEMPOOL_SERVICES.API}/accelerator/estimate`; try { const response = await axios.post(url, req.body, { responseType: 'stream', timeout: 10000 }); for (const key in response.headers) { @@ -97,4 +99,4 @@ class AccelerationRoutes { } } -export default new AccelerationRoutes(); \ No newline at end of file +export default new AccelerationRoutes(); diff --git a/backend/src/api/liquid/liquid.routes.ts b/backend/src/api/liquid/liquid.routes.ts index 5353510f0..de728da6a 100644 --- a/backend/src/api/liquid/liquid.routes.ts +++ b/backend/src/api/liquid/liquid.routes.ts @@ -6,6 +6,8 @@ import icons from './icons'; import { handleError } from '../../utils/api'; import PricesRepository from '../../repositories/PricesRepository'; +const PROXY_PATH_SEGMENT_REGEX = /^(?!\.{1,2}$)[^\p{Cc}/?#\\]{1,256}$/u; + class LiquidRoutes { public initRoutes(app: Application) { app @@ -68,8 +70,13 @@ class LiquidRoutes { } private async $getAssetGroup(req: Request, res: Response) { + if (!PROXY_PATH_SEGMENT_REGEX.test(req.params.id)) { + handleError(req, res, 400, 'Invalid asset group id'); + return; + } + try { - const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.LIQUID_API}/assets/group/${parseInt(req.params.id, 10)}`, + const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.LIQUID_API}/assets/group/${encodeURIComponent(req.params.id)}`, { responseType: 'stream', timeout: 10000 }); response.data.pipe(res); } catch (e) {