mirror of
https://iceshrimp.dev/iceshrimp/iceshrimp
synced 2024-11-27 06:18:06 +09:00
[backend] Check target IP before sending HTTP request
Backported upstream commit "fix(backend): check target IP before sending HTTP request" Co-authored-by: rectcoordsystem <heohyun73@gmail.com> Co-authored-by: anatawa12 <anatawa12@icloud.com>
This commit is contained in:
parent
065590279e
commit
a5f4279d32
60
packages/backend/src/misc/checked-fetch.ts
Normal file
60
packages/backend/src/misc/checked-fetch.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import * as http from "node:http";
|
||||||
|
import * as https from "node:https";
|
||||||
|
import net from "node:net";
|
||||||
|
import { HttpProxyAgent, HttpsProxyAgent } from "hpagent";
|
||||||
|
import config from "@/config/index.js";
|
||||||
|
import IPCIDR from "ip-cidr";
|
||||||
|
import PrivateIp from "private-ip";
|
||||||
|
|
||||||
|
declare module 'node:http' {
|
||||||
|
interface Agent {
|
||||||
|
createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPrivateIp(ip: string): boolean {
|
||||||
|
for (const net of config.allowedPrivateNetworks || []) {
|
||||||
|
const cidr = new IPCIDR(net);
|
||||||
|
if (cidr.contains(ip)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return PrivateIp(ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkConnection(socket: net.Socket) {
|
||||||
|
const address = socket.remoteAddress;
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
if (address && IPCIDR.isValidAddress(address) && isPrivateIp(address)) {
|
||||||
|
socket.destroy(new Error(`Blocked address: ${address}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CheckedHttpAgent extends http.Agent {
|
||||||
|
createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket {
|
||||||
|
const socket = super.createConnection(options, callback).on('connect', () => { checkConnection(socket) });
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CheckedHttpsAgent extends https.Agent {
|
||||||
|
createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket {
|
||||||
|
const socket = super.createConnection(options, callback).on('connect', () => { checkConnection(socket) });
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class CheckedHttpProxyAgent extends HttpProxyAgent {
|
||||||
|
createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket {
|
||||||
|
const socket = super.createConnection(options, callback).on('connect', () => { checkConnection(socket) });
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CheckedHttpsProxyAgent extends HttpsProxyAgent {
|
||||||
|
createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket {
|
||||||
|
const socket = super.createConnection(options, callback).on('connect', () => { checkConnection(socket) });
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
}
|
@ -6,8 +6,6 @@ import { httpAgent, httpsAgent, StatusError } from "./fetch.js";
|
|||||||
import config from "@/config/index.js";
|
import config from "@/config/index.js";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import Logger from "@/services/logger.js";
|
import Logger from "@/services/logger.js";
|
||||||
import IPCIDR from "ip-cidr";
|
|
||||||
import PrivateIp from "private-ip";
|
|
||||||
|
|
||||||
const pipeline = util.promisify(stream.pipeline);
|
const pipeline = util.promisify(stream.pipeline);
|
||||||
|
|
||||||
@ -45,18 +43,6 @@ export async function downloadUrl(url: string, path: string): Promise<void> {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.on("response", (res: Got.Response) => {
|
.on("response", (res: Got.Response) => {
|
||||||
if (
|
|
||||||
(process.env.NODE_ENV === "production" ||
|
|
||||||
process.env.NODE_ENV === "test") &&
|
|
||||||
!config.proxy &&
|
|
||||||
res.ip
|
|
||||||
) {
|
|
||||||
if (isPrivateIp(res.ip)) {
|
|
||||||
logger.warn(`Blocked address: ${res.ip}`);
|
|
||||||
req.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentLength = res.headers["content-length"];
|
const contentLength = res.headers["content-length"];
|
||||||
if (contentLength != null) {
|
if (contentLength != null) {
|
||||||
const size = Number(contentLength);
|
const size = Number(contentLength);
|
||||||
@ -92,13 +78,3 @@ export async function downloadUrl(url: string, path: string): Promise<void> {
|
|||||||
logger.succ(`Download finished: ${chalk.cyan(url)}`);
|
logger.succ(`Download finished: ${chalk.cyan(url)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPrivateIp(ip: string): boolean {
|
|
||||||
for (const net of config.allowedPrivateNetworks || []) {
|
|
||||||
const cidr = new IPCIDR(net);
|
|
||||||
if (cidr.contains(ip)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return PrivateIp(ip);
|
|
||||||
}
|
|
||||||
|
@ -3,8 +3,9 @@ import * as https from "node:https";
|
|||||||
import type { URL } from "node:url";
|
import type { URL } from "node:url";
|
||||||
import CacheableLookup from "cacheable-lookup";
|
import CacheableLookup from "cacheable-lookup";
|
||||||
import fetch from "node-fetch";
|
import fetch from "node-fetch";
|
||||||
import { HttpProxyAgent, HttpsProxyAgent } from "hpagent";
|
|
||||||
import config from "@/config/index.js";
|
import config from "@/config/index.js";
|
||||||
|
import net from "node:net";
|
||||||
|
import {CheckedHttpAgent, CheckedHttpProxyAgent, CheckedHttpsAgent, CheckedHttpsProxyAgent} from "@/misc/checked-fetch.js";
|
||||||
|
|
||||||
export async function getJson(
|
export async function getJson(
|
||||||
url: string,
|
url: string,
|
||||||
@ -132,7 +133,7 @@ const cache = new CacheableLookup({
|
|||||||
/**
|
/**
|
||||||
* Get http non-proxy agent
|
* Get http non-proxy agent
|
||||||
*/
|
*/
|
||||||
const _http = new http.Agent({
|
const _http = new CheckedHttpAgent({
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
keepAliveMsecs: 30 * 1000,
|
keepAliveMsecs: 30 * 1000,
|
||||||
lookup: cache.lookup,
|
lookup: cache.lookup,
|
||||||
@ -141,7 +142,7 @@ const _http = new http.Agent({
|
|||||||
/**
|
/**
|
||||||
* Get https non-proxy agent
|
* Get https non-proxy agent
|
||||||
*/
|
*/
|
||||||
const _https = new https.Agent({
|
const _https = new CheckedHttpsAgent({
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
keepAliveMsecs: 30 * 1000,
|
keepAliveMsecs: 30 * 1000,
|
||||||
lookup: cache.lookup,
|
lookup: cache.lookup,
|
||||||
@ -153,7 +154,7 @@ const maxSockets = Math.max(256, config.deliverJobConcurrency || 128);
|
|||||||
* Get http proxy or non-proxy agent
|
* Get http proxy or non-proxy agent
|
||||||
*/
|
*/
|
||||||
export const httpAgent = config.proxy
|
export const httpAgent = config.proxy
|
||||||
? new HttpProxyAgent({
|
? new CheckedHttpProxyAgent({
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
keepAliveMsecs: 30 * 1000,
|
keepAliveMsecs: 30 * 1000,
|
||||||
maxSockets,
|
maxSockets,
|
||||||
@ -167,7 +168,7 @@ export const httpAgent = config.proxy
|
|||||||
* Get https proxy or non-proxy agent
|
* Get https proxy or non-proxy agent
|
||||||
*/
|
*/
|
||||||
export const httpsAgent = config.proxy
|
export const httpsAgent = config.proxy
|
||||||
? new HttpsProxyAgent({
|
? new CheckedHttpsProxyAgent({
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
keepAliveMsecs: 30 * 1000,
|
keepAliveMsecs: 30 * 1000,
|
||||||
maxSockets,
|
maxSockets,
|
||||||
|
@ -51,40 +51,6 @@ export async function proxyMedia(ctx: Koa.Context) {
|
|||||||
|
|
||||||
if (ctx.status == 429) return;
|
if (ctx.status == 429) return;
|
||||||
|
|
||||||
const { hostname } = new URL(url);
|
|
||||||
let resolvedIps;
|
|
||||||
try {
|
|
||||||
resolvedIps = await promises.resolve(hostname);
|
|
||||||
} catch (error) {
|
|
||||||
ctx.status = 400;
|
|
||||||
ctx.body = { message: "Invalid URL" };
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSSRF = resolvedIps.some((ip) => {
|
|
||||||
if (net.isIPv4(ip)) {
|
|
||||||
const parts = ip.split(".").map(Number);
|
|
||||||
return (
|
|
||||||
parts[0] === 10 ||
|
|
||||||
(parts[0] === 172 && parts[1] >= 16 && parts[1] < 32) ||
|
|
||||||
(parts[0] === 192 && parts[1] === 168) ||
|
|
||||||
parts[0] === 127 ||
|
|
||||||
parts[0] === 0
|
|
||||||
);
|
|
||||||
} else if (net.isIPv6(ip)) {
|
|
||||||
return (
|
|
||||||
ip.startsWith("::") || ip.startsWith("fc00:") || ip.startsWith("fe80:")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isSSRF) {
|
|
||||||
ctx.status = 400;
|
|
||||||
ctx.body = { message: "Access to this URL is not allowed" };
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create temp file
|
// Create temp file
|
||||||
const [path, cleanup] = await createTemp();
|
const [path, cleanup] = await createTemp();
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user