← back to Web Viewer 3877
route /api/fetch + Socket.IO load-url through safeFetch()
21bf7c6bc55a886eba834c54b71c47db7b793486 · 2026-05-19 22:14:14 -0700 · Steve
Replaces the direct axios.get(userUrl) call in fetchWebContent() with
lib/safe-fetch.js. Both POST /api/fetch and io.on('load-url') now share
the same hardened path — no bypass. The API layer translates
err.statusCode (400 / 403 / 502) onto the HTTP response so clients see
the right code instead of a blanket 500.
Closes SSRF surface area at server.js:32-101 + :114-129 (file://,
http://127.0.0.1:9xxx pm2 dashboards, 169.254.169.254 cloud metadata,
localhost:5432 Postgres, etc.).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Files touched
Diff
commit 21bf7c6bc55a886eba834c54b71c47db7b793486
Author: Steve <steve@designerwallcoverings.com>
Date: Tue May 19 22:14:14 2026 -0700
route /api/fetch + Socket.IO load-url through safeFetch()
Replaces the direct axios.get(userUrl) call in fetchWebContent() with
lib/safe-fetch.js. Both POST /api/fetch and io.on('load-url') now share
the same hardened path — no bypass. The API layer translates
err.statusCode (400 / 403 / 502) onto the HTTP response so clients see
the right code instead of a blanket 500.
Closes SSRF surface area at server.js:32-101 + :114-129 (file://,
http://127.0.0.1:9xxx pm2 dashboards, 169.254.169.254 cloud metadata,
localhost:5432 Postgres, etc.).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
server.js | 26 ++++++++++++++++----------
1 file changed, 16 insertions(+), 10 deletions(-)
diff --git a/server.js b/server.js
index ae702f9..89a852a 100644
--- a/server.js
+++ b/server.js
@@ -2,10 +2,10 @@ const express = require('express');
const helmet = require('helmet');
const { createServer } = require('http');
const { Server } = require('socket.io');
-const axios = require('axios');
const cheerio = require('cheerio');
const cors = require('cors');
const path = require('path');
+const { safeFetch } = require('./lib/safe-fetch');
const app = express();
// Security headers via helmet (added 2026-05-04 overnight YOLO loop)
@@ -38,16 +38,21 @@ app.use(express.static('public'));
// Store active conversations
const conversations = new Map();
-// Fetch and process web content
+// Fetch and process web content. All URL validation + SSRF defense is in
+// lib/safe-fetch.js — never call axios.get(userUrl) directly from this file.
async function fetchWebContent(url) {
+ let response;
+ try {
+ response = await safeFetch(url);
+ } catch (err) {
+ // Preserve SSRF-helper status codes for the API layer to translate.
+ if (err.isSsrfBlock) throw err;
+ const wrapped = new Error(`Failed to fetch content: ${err.message}`);
+ wrapped.statusCode = 502;
+ throw wrapped;
+ }
+
try {
- const response = await axios.get(url, {
- headers: {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
- },
- timeout: 10000
- });
-
const $ = cheerio.load(response.data);
// Remove scripts and styles
@@ -106,7 +111,8 @@ app.post('/api/fetch', async (req, res) => {
const content = await fetchWebContent(url);
res.json(content);
} catch (error) {
- res.status(500).json({ error: error.message });
+ const status = error.statusCode || 500;
+ res.status(status).json({ error: error.message });
}
});
← f15b9a4 add lib/safe-fetch.js — SSRF-hardened wrapper around axios
·
back to Web Viewer 3877
·
add test/ssrf.test.js — 25 SSRF guard cases, zero deps 5c4df7c →