diff --git a/backend/src/api/proxy/controllers/proxy.js b/backend/src/api/proxy/controllers/proxy.js index cf7da45..0903828 100644 --- a/backend/src/api/proxy/controllers/proxy.js +++ b/backend/src/api/proxy/controllers/proxy.js @@ -1,35 +1,42 @@ "use strict"; const axios = require("axios"); -module.exports = { - // POST /proxy - async forward(ctx) { - try { - const { - url, - method = "GET", - data = {}, - params = {}, - headers = {}, - } = ctx.request.body; - const response = await axios({ url, method, data, params, headers }); - ctx.send({ status: response.status, data: response.data }); - } catch (error) { - strapi.log.error("Proxy error:", error); - ctx.status = error.response?.status || 500; - ctx.body = { - error: error.message, - details: error.response?.data || null, - }; - } - }, +/** + * Proxy controller — scoped to GovTool API only. + * + * The generic POST /proxy endpoint (forward handler) has been removed. + * It was an unauthenticated open SSRF proxy that allowed any caller to + * make the server fetch arbitrary URLs, including cloud metadata and + * internal services. See issue #4168. + * + * The remaining endpoints forward requests exclusively to the + * GOVTOOL_API_BASE_URL configured in the environment. + */ + +// Allowed path segments for the GovTool proxy. +// Reject any endpoint containing ".." or starting with "/" to prevent +// URL traversal and host-relative redirects. +const BLOCKED_PATTERNS = [/\.\./, /^\/+/, /:\/\//]; +function validateEndpoint(endpoint) { + if (!endpoint) return false; + return !BLOCKED_PATTERNS.some((rx) => rx.test(endpoint)); +} + +module.exports = { // GET /proxy/govtool/:endpoint* async getGovtoolData(ctx) { try { const endpoint = ctx.params.endpoint; if (!endpoint) return ctx.badRequest("Endpoint is required"); + if (!validateEndpoint(endpoint)) { + return ctx.badRequest("Invalid endpoint path"); + } const baseUrl = process.env.GOVTOOL_API_BASE_URL; + if (!baseUrl) { + strapi.log.error("GOVTOOL_API_BASE_URL is not configured"); + return ctx.internalServerError("Proxy target not configured"); + } const fullUrl = `${baseUrl.replace(/\/$/, "")}/${endpoint}`; const response = await axios.get(fullUrl, { params: ctx.query, @@ -53,8 +60,15 @@ module.exports = { try { const endpoint = ctx.params.endpoint; if (!endpoint) return ctx.badRequest("Endpoint is required"); - + if (!validateEndpoint(endpoint)) { + return ctx.badRequest("Invalid endpoint path"); + } const baseUrl = process.env.GOVTOOL_API_BASE_URL; + if (!baseUrl) { + strapi.log.error("GOVTOOL_API_BASE_URL is not configured"); + return ctx.internalServerError("Proxy target not configured"); + } + const fullUrl = `${baseUrl.replace(/\/$/, "")}/${endpoint}`; const response = await axios.post(fullUrl, ctx.request.body, { diff --git a/backend/src/api/proxy/routes/proxy.js b/backend/src/api/proxy/routes/proxy.js index adc9570..4a3a39f 100644 --- a/backend/src/api/proxy/routes/proxy.js +++ b/backend/src/api/proxy/routes/proxy.js @@ -2,32 +2,25 @@ module.exports = { routes: [ - { - method: 'POST', - path: '/proxy', - handler: 'proxy.forward', - config: { - roles: ['authenticated', 'public'], - auth: false - }, - }, + // NOTE: The generic POST /proxy (forward) route has been removed. + // It was an unauthenticated open SSRF proxy. See issue #4168. { method: 'GET', path: '/proxy/govtool/:endpoint*', handler: 'proxy.getGovtoolData', config: { - roles: ['authenticated', 'public'], - auth: false - }, + roles: ['authenticated'], + auth: true + }, }, { method: 'POST', path: '/proxy/govtool/:endpoint*', handler: 'proxy.postGovtoolData', config: { - roles: ['authenticated', 'public'], - auth: false - }, + roles: ['authenticated'], + auth: true + }, }, ], };