[object Object]

← 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 →