Skip to content

Commit 04a3afe

Browse files
gl0bal01claude
andcommitted
fix(security): eliminate all SSRF bypasses, error leaks, and rate limit race
SSRF hardening (H1, H2, H3, M4): - Fix IPv4-mapped IPv6 bypass (::ffff:127.0.0.1 now detected as private) - Check both A and AAAA DNS records (not just one fallback) - Add connect-time IP validation via custom http.Agent (eliminates DNS rebinding) - Re-validate each redirect hop in redirect-chain.js - Apply safe agents to all axios calls in 7 URL-accepting commands Error leak fixes (M1, M2, M3): - aviation.js: stop leaking API error status/data to users - nike.js: stop leaking err.message in HTML report creation - jwt.js: stop leaking raw stderr to users Rate limit fix (M5): - Make check-and-record atomic to prevent concurrent request bypass - Remove separate recordUsage call from index.js Tests: 45 passing (+2 new IPv4-mapped IPv6 tests) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f283831 commit 04a3afe

File tree

14 files changed

+145
-55
lines changed

14 files changed

+145
-55
lines changed

commands/aviation.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,13 +172,15 @@ module.exports = {
172172
return interaction.editReply('Rate limit exceeded. Please try again later.');
173173
}
174174

175-
return interaction.editReply(`API Error (${apiError.response.status}): ${apiError.response.data.error?.info || 'Failed to fetch flight data'}`);
175+
console.error('Aviation API error:', apiError.response?.data);
176+
return interaction.editReply('The aviation API returned an error. Please try again later.');
176177
} else if (apiError.request) {
177178
// No response received
178179
return interaction.editReply('Could not connect to flight data service. Please try again later.');
179180
} else {
180181
// Other error
181-
return interaction.editReply(`Error fetching flight data: ${apiError.message}`);
182+
console.error('Aviation fetch error:', apiError);
183+
return interaction.editReply('An error occurred while fetching flight data. Please try again later.');
182184
}
183185
}
184186
} catch (commandError) {

commands/exif.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const path = require('path');
3030
const { URL } = require('url');
3131
const crypto = require('crypto');
3232
const { isValidUrl, sanitizeInput } = require('../utils/validation');
33-
const { validateUrlNotInternal } = require('../utils/ssrf');
33+
const { validateUrlNotInternal, getSafeAxiosConfig } = require('../utils/ssrf');
3434
const fsPromises = require('fs').promises;
3535

3636
// Ensure the temp directory exists
@@ -195,7 +195,9 @@ async function downloadImageFromUrl(url, filePath) {
195195
reject(new Error('Download timeout after 30 seconds'));
196196
}, 30000);
197197

198+
const agent = getSafeAxiosConfig().httpsAgent;
198199
https.get(url, {
200+
agent,
199201
headers: {
200202
'User-Agent': 'Discord-OSINT-Assistant/2.0 (Image-Analyzer)'
201203
}

commands/extract-links.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const os = require('os');
2323
const { SlashCommandBuilder, AttachmentBuilder } = require('discord.js');
2424
const axios = require('axios');
2525
const cheerio = require('cheerio');
26-
const { validateUrlNotInternal } = require('../utils/ssrf');
26+
const { validateUrlNotInternal, getSafeAxiosConfig } = require('../utils/ssrf');
2727

2828
/**
2929
* Escapes special HTML characters in a string to prevent Cross-Site Scripting (XSS).
@@ -125,7 +125,8 @@ module.exports = {
125125
headers: {
126126
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
127127
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
128-
}
128+
},
129+
...getSafeAxiosConfig()
129130
});
130131
const html = response.data;
131132

commands/favicons.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646

4747
const { SlashCommandBuilder, AttachmentBuilder, EmbedBuilder } = require('discord.js');
4848
const axios = require('axios');
49-
const { validateUrlNotInternal } = require('../utils/ssrf');
49+
const { validateUrlNotInternal, getSafeAxiosConfig } = require('../utils/ssrf');
5050
const cheerio = require('cheerio');
5151
const fs = require('fs').promises;
5252
const fsSync = require('fs');
@@ -239,7 +239,8 @@ const downloadFavicon = async (faviconUrl) => {
239239
'User-Agent': CONFIG.USER_AGENT,
240240
'Accept': 'image/*,*/*;q=0.8'
241241
},
242-
validateStatus: (status) => status >= 200 && status < 400
242+
validateStatus: (status) => status >= 200 && status < 400,
243+
...getSafeAxiosConfig()
243244
});
244245

245246
const contentType = response.headers['content-type'] || '';
@@ -396,7 +397,8 @@ module.exports = {
396397
timeout: CONFIG.REQUEST_TIMEOUT,
397398
headers: { 'User-Agent': CONFIG.USER_AGENT },
398399
maxRedirects: 5,
399-
validateStatus: (status) => status >= 200 && status < 400
400+
validateStatus: (status) => status >= 200 && status < 400,
401+
...getSafeAxiosConfig()
400402
});
401403

402404
html = response.data;

commands/jwt.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -386,11 +386,7 @@ module.exports = {
386386
const errorEmbed = createResultEmbed('❌ Error', errorMessage, errorColor);
387387

388388
if (error.stderr) {
389-
errorEmbed.addFields({
390-
name: 'Technical Details',
391-
value: `\`\`\`\n${error.stderr.substring(0, 500)}\n\`\`\``,
392-
inline: false
393-
});
389+
console.error('JWT tool stderr:', error.stderr);
394390
}
395391

396392
await interaction.editReply({ embeds: [errorEmbed] });

commands/monitor.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
const { SlashCommandBuilder } = require('discord.js');
2727
const axios = require('axios');
2828
const crypto = require('crypto');
29-
const { validateUrlNotInternal } = require('../utils/ssrf');
29+
const { validateUrlNotInternal, getSafeAxiosConfig } = require('../utils/ssrf');
3030

3131
const MONITOR_CHANNEL_ID = process.env.MONITOR_CHANNEL_ID;
3232

@@ -42,7 +42,7 @@ function hashContent(content) {
4242
async function checkWebsite(url, client) {
4343
try {
4444
await validateUrlNotInternal(url);
45-
const response = await axios.get(url);
45+
const response = await axios.get(url, getSafeAxiosConfig());
4646
const newHash = hashContent(response.data);
4747

4848
if (monitoredUrls.has(url) && monitoredUrls.get(url) !== newHash) {

commands/nike.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,8 @@ module.exports = {
337337
console.error('Error creating report file:', err);
338338

339339
// Build a fallback text response
340-
let replyMessage = `Found ${objects.length} results for "${searchString}", but couldn't create HTML report: ${err.message}\n\n`;
340+
console.error('Nike HTML report error:', err);
341+
let replyMessage = `Found ${objects.length} results for "${searchString}", but couldn't create HTML report.\n\n`;
341342

342343
objects.forEach((account, i) => {
343344
const {

commands/recon-web.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, AttachmentBuilder } = require('discord.js');
2323
const axios = require('axios');
24-
const { validateUrlNotInternal } = require('../utils/ssrf');
24+
const { validateUrlNotInternal, getSafeAxiosConfig } = require('../utils/ssrf');
2525
const { isValidDomain } = require('../utils/validation');
2626
const cheerio = require('cheerio');
2727
const fs = require('fs');
@@ -153,7 +153,7 @@ module.exports = {
153153
);
154154
await interaction.editReply({ embeds: [embed] });
155155

156-
const crtshResponse = await axios.get(`https://crt.sh/json?q=${domain}`);
156+
const crtshResponse = await axios.get(`https://crt.sh/json?q=${domain}`, getSafeAxiosConfig());
157157
const certs = crtshResponse.data;
158158

159159
if (certs && certs.length > 0) {
@@ -214,7 +214,7 @@ module.exports = {
214214

215215
const waybackResponse = await axios.get(
216216
`https://web.archive.org/cdx/search/cdx?fl=original&collapse=urlkey&url=*.${domain}`,
217-
{ responseType: 'text' }
217+
{ responseType: 'text', ...getSafeAxiosConfig() }
218218
);
219219

220220
const urls = waybackResponse.data.trim().split('\n');
@@ -285,7 +285,7 @@ module.exports = {
285285
const domainName = sanitizeFilenameComponent(rawDomainName);
286286

287287
// Fetch the webpage content
288-
const response = await axios.get(targetUrl);
288+
const response = await axios.get(targetUrl, getSafeAxiosConfig());
289289
const html = response.data;
290290

291291
// Use cheerio to parse HTML
@@ -337,7 +337,7 @@ module.exports = {
337337
}
338338

339339
// Download the favicon
340-
const faviconResponse = await axios.get(faviconData.url, { responseType: 'arraybuffer' });
340+
const faviconResponse = await axios.get(faviconData.url, { responseType: 'arraybuffer', ...getSafeAxiosConfig() });
341341
const contentType = faviconResponse.headers['content-type'] || '';
342342

343343
// If it's an image, save it

commands/redirect-chain.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
const { SlashCommandBuilder, EmbedBuilder, AttachmentBuilder } = require('discord.js');
1313
const axios = require('axios');
14-
const { validateUrlNotInternal } = require('../utils/ssrf');
14+
const { validateUrlNotInternal, getSafeAxiosConfig } = require('../utils/ssrf');
1515
const { isValidUrl } = require('../utils/validation');
1616
const fs = require('fs');
1717
const path = require('path');
@@ -468,8 +468,12 @@ async function analyzeRedirectChain(url, includeHeaders = false, timeout = 10000
468468
// It's a redirect
469469
const location = response.headers.location;
470470
const redirectUrl = new URL(location, currentUrl).href;
471+
472+
// Re-validate redirect target against SSRF
473+
await validateUrlNotInternal(redirectUrl);
474+
471475
redirectInfo.url = redirectUrl;
472-
476+
473477
redirectChain.push(redirectInfo);
474478
currentUrl = redirectUrl;
475479

@@ -532,7 +536,7 @@ async function robustRequest(url, options, maxRetries = 3) {
532536

533537
for (let i = 0; i < maxRetries; i++) {
534538
try {
535-
return await axios.get(url, options);
539+
return await axios.get(url, { ...options, ...getSafeAxiosConfig() });
536540
} catch (error) {
537541
lastError = error;
538542

@@ -785,7 +789,8 @@ async function analyzeContent(url, headers) {
785789
maxContentLength: 1024 * 1024, // 1MB limit
786790
headers: {
787791
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
788-
}
792+
},
793+
...getSafeAxiosConfig()
789794
});
790795

791796
const html = response.data;
@@ -1060,7 +1065,7 @@ async function notifyWebhook(url, result, suspiciousIndicators) {
10601065
}]
10611066
};
10621067

1063-
await axios.post(process.env.SECURITY_WEBHOOK_URL, payload);
1068+
await axios.post(process.env.SECURITY_WEBHOOK_URL, payload, getSafeAxiosConfig());
10641069
} catch (error) {
10651070
console.error('Failed to send webhook notification:', error);
10661071
}

index.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const { Client, Collection, GatewayIntentBits, Events, MessageFlags } = require(
1616
const fs = require('node:fs');
1717
const path = require('node:path');
1818
const { checkPermission } = require('./utils/permissions');
19-
const { checkRateLimit, recordUsage } = require('./utils/ratelimit');
19+
const { checkRateLimit } = require('./utils/ratelimit');
2020

2121
// Validate environment variables via centralized config
2222
const { loadConfig } = require('./utils/config');
@@ -114,9 +114,6 @@ client.on(Events.InteractionCreate, async interaction => {
114114
return interaction.reply({ content: rateLimitReason, flags: MessageFlags.Ephemeral });
115115
}
116116

117-
// Record usage immediately to prevent concurrent bypass
118-
recordUsage(interaction.user.id, interaction.commandName);
119-
120117
// Log command usage for audit purposes
121118
const timestamp = new Date().toISOString();
122119
const userInfo = `${interaction.user.tag} (${interaction.user.id})`;

0 commit comments

Comments
 (0)