Skip to content

Commit b309487

Browse files
committed
Fix security alerts and dependency updates
1 parent f21d7bc commit b309487

5 files changed

Lines changed: 1196 additions & 1536 deletions

File tree

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
- name: Set up Node.js
2424
uses: actions/setup-node@v4
2525
with:
26-
node-version: 20
26+
node-version: 24
2727
cache: npm
2828

2929
- name: Install dependencies

main.js

Lines changed: 159 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ const path = require('path');
1010
const express = require('express');
1111
const http = require('http');
1212
const https = require('https');
13-
const urlLib = require('url');
1413
const dns = require('dns');
14+
const net = require('net');
1515
const os = require('os');
1616
const fs = require('fs');
1717
const crypto = require('crypto');
@@ -146,85 +146,160 @@ if (isDev) {
146146
}
147147

148148
// ----------------------------- Proxy (CORS + Range) -----------------------------
149-
async function isUrlSafe(targetUrl) {
149+
const proxyTargets = new Map();
150+
const PROXY_TARGET_TTL_MS = 30 * 60 * 1000;
151+
152+
function makeProxyError(message, statusCode = 403) {
153+
const err = new Error(message);
154+
err.statusCode = statusCode;
155+
return err;
156+
}
157+
158+
function cleanupProxyTargets() {
159+
const expiresBefore = Date.now() - PROXY_TARGET_TTL_MS;
160+
for (const [id, entry] of proxyTargets) {
161+
if (entry.createdAt < expiresBefore) proxyTargets.delete(id);
162+
}
163+
}
164+
165+
function registerProxyTarget(rawUrl) {
166+
cleanupProxyTargets();
167+
const id = crypto.randomBytes(16).toString('hex');
168+
proxyTargets.set(id, { url: rawUrl, createdAt: Date.now() });
169+
return id;
170+
}
171+
172+
function isBlockedIp(address) {
173+
const family = net.isIP(address);
174+
if (family === 4) {
175+
const parts = address.split('.').map(part => Number(part));
176+
if (parts.length !== 4 || parts.some(part => !Number.isInteger(part) || part < 0 || part > 255)) return true;
177+
const [a, b, c] = parts;
178+
return (
179+
a === 0 ||
180+
a === 10 ||
181+
a === 127 ||
182+
a === 169 && b === 254 ||
183+
a === 172 && b >= 16 && b <= 31 ||
184+
a === 192 && b === 168 ||
185+
a === 192 && b === 0 && c === 0 ||
186+
a === 192 && b === 0 && c === 2 ||
187+
a === 198 && (b === 18 || b === 19) ||
188+
a === 198 && b === 51 && c === 100 ||
189+
a === 203 && b === 0 && c === 113 ||
190+
a >= 224
191+
);
192+
}
193+
194+
if (family === 6) {
195+
const lower = address.toLowerCase();
196+
const mappedIpv4 = lower.match(/::ffff:(\d+\.\d+\.\d+\.\d+)$/);
197+
if (mappedIpv4) return isBlockedIp(mappedIpv4[1]);
198+
return (
199+
lower === '::' ||
200+
lower === '::1' ||
201+
lower.startsWith('fc') ||
202+
lower.startsWith('fd') ||
203+
lower.startsWith('fe80:') ||
204+
lower.startsWith('ff')
205+
);
206+
}
207+
208+
return true;
209+
}
210+
211+
function isBlockedHostname(hostname) {
212+
const normalized = String(hostname || '').replace(/^\[|\]$/g, '').toLowerCase();
213+
if (!normalized) return true;
214+
if (
215+
normalized === 'localhost' ||
216+
normalized.endsWith('.localhost') ||
217+
normalized.endsWith('.local') ||
218+
normalized.endsWith('.lan')
219+
) {
220+
return true;
221+
}
222+
return net.isIP(normalized) ? isBlockedIp(normalized) : false;
223+
}
224+
225+
async function resolvePublicAddresses(hostname) {
226+
const family = net.isIP(hostname);
227+
const records = family
228+
? [{ address: hostname, family }]
229+
: await dns.promises.lookup(hostname, { all: true, verbatim: true });
230+
231+
if (!records.length) throw makeProxyError('Proxy target could not be resolved.', 403);
232+
if (records.some(record => isBlockedIp(record.address))) {
233+
throw makeProxyError('Proxy target resolves to a private or reserved network.', 403);
234+
}
235+
return records;
236+
}
237+
238+
async function buildSafeProxyRequest(rawUrl, rangeHeader) {
150239
try {
151-
const parsed = urlLib.parse(targetUrl);
152-
if (!['http:', 'https:'].includes(parsed.protocol)) return false;
153-
154-
const hostname = parsed.hostname;
155-
if (!hostname) return false;
156-
157-
// Check known loopback/private patterns on hostname (before DNS)
158-
if (hostname.toLowerCase() === 'localhost') return false;
159-
if (hostname === '::1' || hostname === '[::1]') return false;
160-
if (hostname.startsWith('127.')) return false;
161-
if (hostname.startsWith('10.')) return false;
162-
if (hostname.startsWith('192.168.')) return false;
163-
if (hostname.startsWith('169.254.')) return false;
164-
// 172.16.x.x - 172.31.x.x
165-
const parts = hostname.split('.');
166-
if (parts.length === 4 && parts[0] === '172') {
167-
const second = parseInt(parts[1], 10);
168-
if (second >= 16 && second <= 31) return false;
240+
const parsed = new URL(rawUrl);
241+
if (!['http:', 'https:'].includes(parsed.protocol)) {
242+
throw makeProxyError('Only http and https proxy targets are allowed.', 403);
169243
}
170-
171-
// Resolve Hostname to IP to catch domains pointing to private IPs
172-
return new Promise((resolve) => {
173-
dns.lookup(hostname, { family: 4 }, (err, address) => {
174-
if (err) {
175-
// Determine if we should fail open or closed. Failing closed (reject) is safer.
176-
return resolve(false);
177-
}
178-
if (!address) return resolve(false);
179-
180-
// Re-check resolved IP
181-
if (address.startsWith('127.')) return resolve(false);
182-
if (address.startsWith('10.')) return resolve(false);
183-
if (address.startsWith('192.168.')) return resolve(false);
184-
if (address.startsWith('169.254.')) return resolve(false);
185-
186-
const ipParts = address.split('.');
187-
if (ipParts.length === 4 && ipParts[0] === '172') {
188-
const second = parseInt(ipParts[1], 10);
189-
if (second >= 16 && second <= 31) return resolve(false);
190-
}
191-
192-
resolve(true);
193-
});
194-
});
244+
if (parsed.username || parsed.password) {
245+
throw makeProxyError('Proxy targets with credentials are not allowed.', 403);
246+
}
247+
if (isBlockedHostname(parsed.hostname)) {
248+
throw makeProxyError('Proxy target host is private or reserved.', 403);
249+
}
250+
251+
const records = await resolvePublicAddresses(parsed.hostname);
252+
const selected = records[0];
253+
const headers = {
254+
'Host': parsed.host,
255+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
256+
'Referer': 'https://www.youtube.com/',
257+
'Origin': 'https://www.youtube.com',
258+
'Accept': '*/*',
259+
'Accept-Language': 'en-US,en;q=0.9'
260+
};
261+
if (rangeHeader) headers['Range'] = String(rangeHeader).slice(0, 128);
262+
263+
return {
264+
client: parsed.protocol === 'https:' ? https : http,
265+
displayUrl: parsed.href,
266+
options: {
267+
protocol: parsed.protocol,
268+
hostname: selected.address,
269+
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
270+
path: `${parsed.pathname}${parsed.search}`,
271+
method: 'GET',
272+
headers,
273+
servername: parsed.hostname,
274+
family: selected.family,
275+
lookup: (hostname, options, callback) => callback(null, selected.address, selected.family),
276+
timeout: 30000
277+
}
278+
};
195279
} catch (e) {
196-
return false;
280+
if (e.statusCode) throw e;
281+
throw makeProxyError('Invalid proxy target URL.', 400);
197282
}
198283
}
199284

200285
async function startProxy() {
201286
const exapp = express();
202287

203-
exapp.get('/proxy', async (req, res) => {
204-
const target = req.query.url;
205-
if (!target) return res.status(400).send('Missing url');
288+
exapp.get('/proxy/:id', async (req, res) => {
289+
const entry = proxyTargets.get(req.params.id);
290+
if (!entry) return res.status(404).send('Proxy target expired or not found.');
206291

207-
// SSRF Protection
208-
const safe = await isUrlSafe(target);
209-
if (!safe) {
210-
console.warn('[Proxy] Blocked unsafe URL:', target);
211-
return res.status(403).send('Forbidden: Access to private resources or invalid URL is denied.');
292+
let request;
293+
try {
294+
request = await buildSafeProxyRequest(entry.url, req.headers['range']);
295+
} catch (err) {
296+
console.warn('[Proxy] Blocked target:', err.message);
297+
return res.status(err.statusCode || 403).send(err.message);
212298
}
213299

214-
const headers = {};
215-
if (req.headers['range']) headers['Range'] = req.headers['range'];
216-
headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
217-
headers['Referer'] = 'https://www.youtube.com/';
218-
headers['Origin'] = 'https://www.youtube.com';
219-
headers['Accept'] = '*/*';
220-
headers['Accept-Language'] = 'en-US,en;q=0.9';
221-
222-
const parsed = urlLib.parse(target);
223-
const client = parsed.protocol === 'https:' ? https : http;
300+
console.log('[Proxy] Fetching:', request.displayUrl.substring(0, 80) + '...');
224301

225-
console.log('[Proxy] Fetching:', target.substring(0, 80) + '...');
226-
227-
const proxReq = client.get(target, { headers }, proxRes => {
302+
const proxReq = request.client.get(request.options, proxRes => {
228303
console.log('[Proxy] Response status:', proxRes.statusCode, 'Content-Type:', proxRes.headers['content-type']);
229304

230305
// Content-Type'ı düzelt (video için)
@@ -256,13 +331,17 @@ async function startProxy() {
256331
if (!res.headersSent) res.status(502).send('Proxy error: ' + err.message);
257332
});
258333

334+
proxReq.setTimeout(30000, () => {
335+
proxReq.destroy(new Error('Proxy request timed out.'));
336+
});
337+
259338
req.on('close', () => {
260339
proxReq.destroy();
261340
});
262341
});
263342

264343
return new Promise((resolve) => {
265-
const server = exapp.listen(0, () => {
344+
const server = exapp.listen(0, '127.0.0.1', () => {
266345
const { port } = server.address();
267346
console.log('[Proxy] Started on port', port);
268347
resolve({ server, port });
@@ -428,7 +507,6 @@ ipcMain.handle('fetchVideoInfo', async (event, url, cookiesOptions) => {
428507
// yt-dlp seçenekleri
429508
const ytdlpOptions = {
430509
dumpSingleJson: true,
431-
noCheckCertificates: true,
432510
noWarnings: true,
433511
preferFreeFormats: false
434512
};
@@ -486,7 +564,8 @@ ipcMain.handle('fetchVideoInfo', async (event, url, cookiesOptions) => {
486564
});
487565

488566
const previewFormat = progressive[0] || videoOnly[0] || null;
489-
const previewUrl = previewFormat ? `http://127.0.0.1:${proxyPort}/proxy?url=${encodeURIComponent(previewFormat.url)}` : null;
567+
const previewId = previewFormat ? registerProxyTarget(previewFormat.url) : null;
568+
const previewUrl = previewId ? `http://127.0.0.1:${proxyPort}/proxy/${previewId}` : null;
490569

491570
console.log('[Preview] All formats count:', formats.length);
492571
console.log('[Preview] Progressive (playable) formats:', progressive.length);
@@ -512,7 +591,6 @@ ipcMain.handle('fetchVideoInfo', async (event, url, cookiesOptions) => {
512591
tbr: f.tbr,
513592
abr: f.abr,
514593
asr: f.asr,
515-
url: f.url || null,
516594
container: f.container || null
517595
}));
518596

@@ -607,7 +685,6 @@ ipcMain.handle('startExport', async (event, params) => {
607685
const ytdlpDownloadOptions = {
608686
format: ytFormat,
609687
'no-warnings': true,
610-
'no-check-certificates': true,
611688
output: path.join(tempDir, 'dl.%(ext)s')
612689
};
613690

@@ -797,10 +874,18 @@ ipcMain.handle('revealInFolder', async (event, filePath) => {
797874
}
798875
});
799876

877+
function sanitizeExternalUrl(rawUrl) {
878+
const parsed = new URL(String(rawUrl || ''));
879+
if (!['http:', 'https:'].includes(parsed.protocol)) {
880+
throw new Error('Only http and https links can be opened.');
881+
}
882+
return parsed.href;
883+
}
884+
800885
// ----------------------------- IPC: Harici URL Aç -----------------------------
801886
ipcMain.handle('openExternal', async (event, url) => {
802887
try {
803-
await shell.openExternal(url);
888+
await shell.openExternal(sanitizeExternalUrl(url));
804889
return { ok: true };
805890
} catch (e) {
806891
return { ok: false, error: String(e) };
@@ -841,4 +926,4 @@ ipcMain.handle('chooseCookiesFile', async (event) => {
841926
ipcMain.handle('getDefaultDocumentsPath', async () => {
842927
const documentsPath = app.getPath('documents');
843928
return { ok: true, path: documentsPath };
844-
});
929+
});

0 commit comments

Comments
 (0)