From 79533ceec9aebd15a8088f4afe2474f9bd153c8c Mon Sep 17 00:00:00 2001 From: mei23 Date: Tue, 26 Mar 2024 21:05:29 +0900 Subject: [PATCH] fix (backend): improve URL check https://github.com/mei23/misskey/commit/13ea67bee40b394906f55e75d5d80d6e4f7fa171 https://github.com/mei23/misskey/commit/da12d5b079b690d3705a76919ed817f6dfc03ac8 Co-authored-by: naskya --- packages/backend/src/misc/download-url.ts | 11 ++++++++++ packages/backend/src/misc/fetch.ts | 9 +++++++++ packages/backend/src/misc/is-valid-url.ts | 20 +++++++++++++++++++ .../backend/src/remote/activitypub/request.ts | 7 ++++++- 4 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 packages/backend/src/misc/is-valid-url.ts diff --git a/packages/backend/src/misc/download-url.ts b/packages/backend/src/misc/download-url.ts index 83680c175..ab04e8aa9 100644 --- a/packages/backend/src/misc/download-url.ts +++ b/packages/backend/src/misc/download-url.ts @@ -8,10 +8,15 @@ import chalk from "chalk"; import Logger from "@/services/logger.js"; import IPCIDR from "ip-cidr"; import PrivateIp from "private-ip"; +import { isValidUrl } from "./is-valid-url.js"; const pipeline = util.promisify(stream.pipeline); export async function downloadUrl(url: string, path: string): Promise { + if (!isValidUrl(url)) { + throw new StatusError("Invalid URL", 400); + } + const logger = new Logger("download"); logger.info(`Downloading ${chalk.cyan(url)} ...`); @@ -44,6 +49,12 @@ export async function downloadUrl(url: string, path: string): Promise { limit: 0, }, }) + .on("redirect", (res: Got.Response, opts: Got.NormalizedOptions) => { + if (!isValidUrl(opts.url)) { + logger.warn(`Invalid URL: ${opts.url}`); + req.destroy(); + } + }) .on("response", (res: Got.Response) => { if ( (process.env.NODE_ENV === "production" || diff --git a/packages/backend/src/misc/fetch.ts b/packages/backend/src/misc/fetch.ts index e47ef0d47..36027de50 100644 --- a/packages/backend/src/misc/fetch.ts +++ b/packages/backend/src/misc/fetch.ts @@ -5,6 +5,7 @@ import CacheableLookup from "cacheable-lookup"; import fetch from "node-fetch"; import { HttpProxyAgent, HttpsProxyAgent } from "hpagent"; import config from "@/config/index.js"; +import { isValidUrl } from "./is-valid-url.js"; export async function getJson( url: string, @@ -58,6 +59,10 @@ export async function getResponse(args: { timeout?: number; size?: number; }) { + if (!isValidUrl(args.url)) { + throw new StatusError("Invalid URL", 400); + } + const timeout = args.timeout || 10 * 1000; const controller = new AbortController(); @@ -83,6 +88,10 @@ export async function getResponse(args: { ); } + if (res.redirected && !isValidUrl(res.url)) { + throw new StatusError("Invalid URL", 400); + } + return res; } diff --git a/packages/backend/src/misc/is-valid-url.ts b/packages/backend/src/misc/is-valid-url.ts new file mode 100644 index 000000000..5aebefcb7 --- /dev/null +++ b/packages/backend/src/misc/is-valid-url.ts @@ -0,0 +1,20 @@ +export function isValidUrl(url: string | URL | undefined): boolean { + if (process.env.NODE_ENV !== "production") return true; + + try { + if (url == null) return false; + + const u = typeof url === "string" ? new URL(url) : url; + if (!u.protocol.match(/^https?:$/) || u.hostname === "unix") { + return false; + } + + if (u.port !== "" && !["80", "443"].includes(u.port)) { + return false; + } + + return true; + } catch { + return false; + } +} diff --git a/packages/backend/src/remote/activitypub/request.ts b/packages/backend/src/remote/activitypub/request.ts index 9fc2a8115..07ccbf4e8 100644 --- a/packages/backend/src/remote/activitypub/request.ts +++ b/packages/backend/src/remote/activitypub/request.ts @@ -1,10 +1,11 @@ import config from "@/config/index.js"; import { getUserKeypair } from "@/misc/keypair-store.js"; import type { User, ILocalUser } from "@/models/entities/user.js"; -import { getResponse } from "@/misc/fetch.js"; +import { StatusError, getResponse } from "@/misc/fetch.js"; import { createSignedPost, createSignedGet } from "./ap-request.js"; import type { Response } from "node-fetch"; import type { IObject } from "./type.js"; +import { isValidUrl } from "@/misc/is-valid-url.js"; export default async (user: { id: User["id"] }, url: string, object: any) => { const body = JSON.stringify(object); @@ -37,6 +38,10 @@ export default async (user: { id: User["id"] }, url: string, object: any) => { * @param url URL to fetch */ export async function apGet(url: string, user?: ILocalUser): Promise { + if (!isValidUrl(url)) { + throw new StatusError("Invalid URL", 400); + } + let res: Response; if (user != null) {