CVE-2021-39195 (TypeScript, published 2021-09-07T19:15Z)
Description: value: Misskey is an open source, decentralized microblogging platform. In affected versions a Server-Side Request Forgery vulnerability exists in "Upload from URL" and remote attachment handling. This could result in the disclosure of non-public information within the internal network. This has been fixed in 12.90.0. However, if you are using a proxy, you will need to take additional measures. As a workaround this exploit may be avoided by appropriately restricting access to private networks from the host where the application is running.
Severity: MEDIUM (CVSS v3 base score: 6.5)
Weakness: CWE-918 \u2014 Server-Side Request Forgery (SSRF)
The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
Repository: https://github.com/misskey-dev/misskey
Files changed:
.config/example.yml
package.json
src/config/types.ts
src/misc/download-url.ts
src/server/file/send-drive-file.ts
src/server/proxy/proxy-media.ts
yarn.lock
Commit message: Tune download (#2)

* s2-2

* allowedPrivateNetworks

* style

* Proxyの間にあると誤解しそうなのでconfigの記述順を変更

* Fix error handler

Vulnerable code:
import { URL } from 'url';
import fetch from 'node-fetch';
import { getAgentByUrl } from './fetch';
import { AbortController } from 'abort-controller';
	const controller = new AbortController();
	setTimeout(() => {
		controller.abort();
	}, 60 * 1000);
	const response = await fetch(new URL(url).href, {
		timeout: 10 * 1000,
		signal: controller.signal,
		agent: getAgentByUrl,
	if (!response.ok) {
		logger.error(`Got ${response.status} (${url})`);
		throw response.status;
	}

	await pipeline(response.body, fs.createWriteStream(path));
				serverLogger.error(e);
				if (typeof e == 'number' && e >= 400 && e < 500) {
					ctx.status = e;
		if (typeof e == 'number' && e >= 400 && e < 500) {
			ctx.status = e;

Fixed code:

#allowedPrivateNetworks: [
#  '127.0.0.1/32'
#]
		"ip-cidr": "3.0.4",
		"private-ip": "2.2.1",
	allowedPrivateNetworks?: string[];

import got, * as Got from 'got';
import { httpAgent, httpsAgent } from './fetch';
import * as IPCIDR from 'ip-cidr';
const PrivateIp = require('private-ip');
	const timeout = 30 * 1000;
	const operationTimeout = 60 * 1000;

	const req = got.stream(url, {
		timeout: {
			lookup: timeout,
			connect: timeout,
			secureConnect: timeout,
			socket: timeout,	// read timeout
			response: timeout,
			send: timeout,
			request: operationTimeout,	// whole operation timeout
		},
		agent: {
			http: httpAgent,
			https: httpsAgent,
		},
		retry: 0,
	}).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();
			}
		}
	}).on('error', (e: any) => {
		if (e.name === 'HTTPError') {
			const statusCode = e.response?.statusCode;
			const statusMessage = e.response?.statusMessage;
			e.name = `StatusError`;
			e.statusCode = statusCode;
			e.message = `${statusCode} ${statusMessage}`;
		}
	await pipeline(req, fs.createWriteStream(path));

function isPrivateIp(ip: string) {
	for (const net of config.allowedPrivateNetworks || []) {
		const cidr = new IPCIDR(net);
		if (cidr.contains(ip)) {
			return false;
		}
	}

	return PrivateIp(ip);
}
				serverLogger.error(e.statusCode);
				if (typeof e.statusCode === 'number' && e.statusCode >= 400 && e.statusCode < 500) {
					ctx.status = e.statusCode;
		if (typeof e.statusCode === 'number' && e.statusCode >= 400 && e.statusCode < 500) {
			ctx.status = e.statusCode;
ip-address@^7.1.0:
  version "7.1.0"
  resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-7.1.0.tgz#4a9c699e75b51cbeb18b38de8ed216efa1a490c5"
  integrity sha512-V9pWC/VJf2lsXqP7IWJ+pe3P1/HCYGBMZrrnT62niLGjAfCbeiwXMUxaeHvnVlz19O27pvXP4azs+Pj/A0x+SQ==
  dependencies:
    jsbn "1.1.0"
    sprintf-js "1.1.2"

ip-cidr@3.0.4:
  version "3.0.4"
  resolved "https://registry.yarnpkg.com/ip-cidr/-/ip-cidr-3.0.4.tgz#a915c47e00f47ea8d5f8ed662ea6161471c44375"
  integrity sha512-pKNiqmBlTvEkhaLAa3+FOmYSY0/jjADVxxjA3NbujZZTT8mjLI90Q+6mwg6kd0fNm0RuAOkWJ1u1a/ETmlrPNQ==
  dependencies:
    ip-address "^7.1.0"
    jsbn "^1.1.0"

ip-regex@^4.3.0:
  version "4.3.0"
  resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5"
  integrity sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==

jsbn@1.1.0, jsbn@^1.1.0:
  version "1.1.0"
  resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040"
  integrity sha1-sBMHyym2GKHtJux56RH4A8TaAEA=

netmask@^2.0.2:
  version "2.0.2"
  resolved "https://registry.yarnpkg.com/netmask/-/netmask-2.0.2.tgz#8b01a07644065d536383835823bc52004ebac5e7"
  integrity sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==

private-ip@2.2.1:
  version "2.2.1"
  resolved "https://registry.yarnpkg.com/private-ip/-/private-ip-2.2.1.tgz#4fe167d04e12eca5c67cdcbd3224e86b38c79253"
  integrity sha512-jN1WT/br/VNW9xEcwHr6DjtOKxQ5qOIqmh7o+co2TWgq56pZJw99iO3UT1tWdfgsQiyK9FqG4ji3ykwpjFqITA==
  dependencies:
    ip-regex "^4.3.0"
    netmask "^2.0.2"

sprintf-js@1.1.2:
  version "1.1.2"
  resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673"
  integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==