Skip to content

[LEGIT] Fix - js/request-forgery - #114

Open
liorn-test-app[bot] wants to merge 1 commit into
masterfrom
legit-security-6f73dc
Open

[LEGIT] Fix - js/request-forgery#114
liorn-test-app[bot] wants to merge 1 commit into
masterfrom
legit-security-6f73dc

Conversation

@liorn-test-app

Copy link
Copy Markdown

🔍 The problem

Server-side request forgery
See issue in Legit

🔒 Fix Details

Fixed a Server-Side Request Forgery (SSRF) vulnerability where user-controlled query parameters (req.query.url and req.query.symbol) were concatenated and passed directly to needle.get() without validation. An attacker could supply malicious URLs to access internal services, cloud metadata endpoints (e.g., 169.254.169.254), or local file systems.

The fix adds a validation function isAllowedStockApiUrl() that enforces:

  • Protocol restriction to http:// or https:// only
  • Domain allowlist matching trusted stock API providers
  • Rejection of localhost, loopback addresses, private IP ranges (RFC 1918), and cloud metadata endpoints

The validation executes before the needle.get() call, returning a 400 error for any URL that fails validation, preventing the server from making requests to unauthorized destinations.

--- a/app/routes/research.js
+++ b/app/routes/research.js
@@ -6,10 +6,54 @@
 
     const researchDAO = new ResearchDAO(db);
 
+    const isAllowedStockApiUrl = (urlString) => {
+        try {
+            const url = new URL(urlString);
+            
+            // Check protocol is http or https
+            if (url.protocol !== 'https:' && url.protocol !== 'http:') {
+                return false;
+            }
+            
+            // Allowlist of permitted stock API domains
+            const allowedDomains = ['api.example-stock-service.com', 'quotes.trusted-provider.com'];
+            if (!allowedDomains.includes(url.hostname)) {
+                return false;
+            }
+            
+            // Reject localhost and loopback
+            if (url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '::1') {
+                return false;
+            }
+            
+            // Reject private IP ranges
+            if (url.hostname.startsWith('10.') || 
+                url.hostname.startsWith('192.168.') ||
+                (url.hostname.startsWith('172.') && 
+                 parseInt(url.hostname.split('.')[1]) >= 16 && 
+                 parseInt(url.hostname.split('.')[1]) <= 31)) {
+                return false;
+            }
+            
+            // Reject cloud metadata endpoints
+            if (url.hostname === '169.254.169.254' || url.hostname === 'fd00:ec2::254') {
+                return false;
+            }
+            
+            return true;
+        } catch (e) {
+            return false;
+        }
+    };
+
     this.displayResearch = (req, res) => {
         
         if (req.query.symbol) {
             const url = req.query.url+req.query.symbol; 
+            if (!isAllowedStockApiUrl(url)) {
+                res.status(400);
+                return res.send('Invalid stock API URL');
+            }
             return needle.get(url, (error, newResponse) => {
                 if (!error && newResponse.statusCode == 200)
                     res.writeHead(200, {'Content-Type': 'text/html'});

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
C Maintainability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

💡 Need a hand with PR review? Try Gitar by Sonar!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants