Skip to content

Commit

Permalink
Merge pull request ChatGPTNextWeb#4285 from fred-bf/fix/cors-ssrf
Browse files Browse the repository at this point in the history
[Bugfix] Fix CORS SSRF security issue
  • Loading branch information
fred-bf authored Mar 13, 2024
2 parents 61ce386 + 99aa064 commit a22141c
Show file tree
Hide file tree
Showing 7 changed files with 226 additions and 106 deletions.
43 changes: 0 additions & 43 deletions app/api/cors/[...path]/route.ts

This file was deleted.

73 changes: 73 additions & 0 deletions app/api/upstash/[action]/[...key]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { NextRequest, NextResponse } from "next/server";

async function handle(
req: NextRequest,
{ params }: { params: { action: string; key: string[] } },
) {
const requestUrl = new URL(req.url);
const endpoint = requestUrl.searchParams.get("endpoint");

if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
}
const [...key] = params.key;
// only allow to request to *.upstash.io
if (!endpoint || !new URL(endpoint).hostname.endsWith(".upstash.io")) {
return NextResponse.json(
{
error: true,
msg: "you are not allowed to request " + params.key.join("/"),
},
{
status: 403,
},
);
}

// only allow upstash get and set method
if (params.action !== "get" && params.action !== "set") {
console.log("[Upstash Route] forbidden action ", params.action);
return NextResponse.json(
{
error: true,
msg: "you are not allowed to request " + params.action,
},
{
status: 403,
},
);
}

const targetUrl = `${endpoint}/${params.action}/${params.key.join("/")}`;

const method = req.method;
const shouldNotHaveBody = ["get", "head"].includes(
method?.toLowerCase() ?? "",
);

const fetchOptions: RequestInit = {
headers: {
authorization: req.headers.get("authorization") ?? "",
},
body: shouldNotHaveBody ? null : req.body,
method,
// @ts-ignore
duplex: "half",
};

console.log("[Upstash Proxy]", targetUrl, fetchOptions);
const fetchResult = await fetch(targetUrl, fetchOptions);

console.log("[Any Proxy]", targetUrl, {
status: fetchResult.status,
statusText: fetchResult.statusText,
});

return fetchResult;
}

export const POST = handle;
export const GET = handle;
export const OPTIONS = handle;

export const runtime = "edge";
112 changes: 112 additions & 0 deletions app/api/webdav/[...path]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { NextRequest, NextResponse } from "next/server";
import { STORAGE_KEY } from "../../../constant";
async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
) {
if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
}
const folder = STORAGE_KEY;
const fileName = `${folder}/backup.json`;

const requestUrl = new URL(req.url);
let endpoint = requestUrl.searchParams.get("endpoint");
if (!endpoint?.endsWith("/")) {
endpoint += "/";
}
const endpointPath = params.path.join("/");

// only allow MKCOL, GET, PUT
if (req.method !== "MKCOL" && req.method !== "GET" && req.method !== "PUT") {
return NextResponse.json(
{
error: true,
msg: "you are not allowed to request " + params.path.join("/"),
},
{
status: 403,
},
);
}

// for MKCOL request, only allow request ${folder}
if (
req.method == "MKCOL" &&
!new URL(endpointPath).pathname.endsWith(folder)
) {
return NextResponse.json(
{
error: true,
msg: "you are not allowed to request " + params.path.join("/"),
},
{
status: 403,
},
);
}

// for GET request, only allow request ending with fileName
if (
req.method == "GET" &&
!new URL(endpointPath).pathname.endsWith(fileName)
) {
return NextResponse.json(
{
error: true,
msg: "you are not allowed to request " + params.path.join("/"),
},
{
status: 403,
},
);
}

// for PUT request, only allow request ending with fileName
if (
req.method == "PUT" &&
!new URL(endpointPath).pathname.endsWith(fileName)
) {
return NextResponse.json(
{
error: true,
msg: "you are not allowed to request " + params.path.join("/"),
},
{
status: 403,
},
);
}

const targetUrl = `${endpoint + endpointPath}`;

const method = req.method;
const shouldNotHaveBody = ["get", "head"].includes(
method?.toLowerCase() ?? "",
);

const fetchOptions: RequestInit = {
headers: {
authorization: req.headers.get("authorization") ?? "",
},
body: shouldNotHaveBody ? null : req.body,
method,
// @ts-ignore
duplex: "half",
};

const fetchResult = await fetch(targetUrl, fetchOptions);

console.log("[Any Proxy]", targetUrl, {
status: fetchResult.status,
statusText: fetchResult.statusText,
});

return fetchResult;
}

export const POST = handle;
export const GET = handle;
export const OPTIONS = handle;

export const runtime = "edge";
2 changes: 1 addition & 1 deletion app/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export enum Path {
}

export enum ApiPath {
Cors = "/api/cors",
Cors = "",
OpenAI = "/api/openai",
}

Expand Down
34 changes: 20 additions & 14 deletions app/utils/cloud/upstash.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { STORAGE_KEY } from "@/app/constant";
import { SyncStore } from "@/app/store/sync";
import { corsFetch } from "../cors";
import { chunks } from "../format";

export type UpstashConfig = SyncStore["upstash"];
Expand All @@ -18,10 +17,9 @@ export function createUpstashClient(store: SyncStore) {
return {
async check() {
try {
const res = await corsFetch(this.path(`get/${storeKey}`), {
const res = await fetch(this.path(`get/${storeKey}`, proxyUrl), {
method: "GET",
headers: this.headers(),
proxyUrl,
});
console.log("[Upstash] check", res.status, res.statusText);
return [200].includes(res.status);
Expand All @@ -32,10 +30,9 @@ export function createUpstashClient(store: SyncStore) {
},

async redisGet(key: string) {
const res = await corsFetch(this.path(`get/${key}`), {
const res = await fetch(this.path(`get/${key}`, proxyUrl), {
method: "GET",
headers: this.headers(),
proxyUrl,
});

console.log("[Upstash] get key = ", key, res.status, res.statusText);
Expand All @@ -45,11 +42,10 @@ export function createUpstashClient(store: SyncStore) {
},

async redisSet(key: string, value: string) {
const res = await corsFetch(this.path(`set/${key}`), {
const res = await fetch(this.path(`set/${key}`, proxyUrl), {
method: "POST",
headers: this.headers(),
body: value,
proxyUrl,
});

console.log("[Upstash] set key = ", key, res.status, res.statusText);
Expand Down Expand Up @@ -84,18 +80,28 @@ export function createUpstashClient(store: SyncStore) {
Authorization: `Bearer ${config.apiKey}`,
};
},
path(path: string) {
let url = config.endpoint;

if (!url.endsWith("/")) {
url += "/";
path(path: string, proxyUrl: string = "") {
if (!path.endsWith("/")) {
path += "/";
}

if (path.startsWith("/")) {
path = path.slice(1);
}

return url + path;
if (proxyUrl.length > 0 && !proxyUrl.endsWith("/")) {
proxyUrl += "/";
}

let url;
if (proxyUrl.length > 0 || proxyUrl === "/") {
let u = new URL(proxyUrl + "/api/upstash/" + path);
// add query params
u.searchParams.append("endpoint", config.endpoint);
url = u.toString();
} else {
url = "/api/upstash/" + path + "?endpoint=" + config.endpoint;
}
return url;
},
};
}
34 changes: 20 additions & 14 deletions app/utils/cloud/webdav.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { STORAGE_KEY } from "@/app/constant";
import { SyncStore } from "@/app/store/sync";
import { corsFetch } from "../cors";

export type WebDAVConfig = SyncStore["webdav"];
export type WebDavClient = ReturnType<typeof createWebDavClient>;
Expand All @@ -15,10 +14,9 @@ export function createWebDavClient(store: SyncStore) {
return {
async check() {
try {
const res = await corsFetch(this.path(folder), {
const res = await fetch(this.path(folder, proxyUrl), {
method: "MKCOL",
headers: this.headers(),
proxyUrl,
});
console.log("[WebDav] check", res.status, res.statusText);
return [201, 200, 404, 301, 302, 307, 308].includes(res.status);
Expand All @@ -30,10 +28,9 @@ export function createWebDavClient(store: SyncStore) {
},

async get(key: string) {
const res = await corsFetch(this.path(fileName), {
const res = await fetch(this.path(fileName, proxyUrl), {
method: "GET",
headers: this.headers(),
proxyUrl,
});

console.log("[WebDav] get key = ", key, res.status, res.statusText);
Expand All @@ -42,11 +39,10 @@ export function createWebDavClient(store: SyncStore) {
},

async set(key: string, value: string) {
const res = await corsFetch(this.path(fileName), {
const res = await fetch(this.path(fileName, proxyUrl), {
method: "PUT",
headers: this.headers(),
body: value,
proxyUrl,
});

console.log("[WebDav] set key = ", key, res.status, res.statusText);
Expand All @@ -59,18 +55,28 @@ export function createWebDavClient(store: SyncStore) {
authorization: `Basic ${auth}`,
};
},
path(path: string) {
let url = config.endpoint;

if (!url.endsWith("/")) {
url += "/";
path(path: string, proxyUrl: string = "") {
if (!path.endsWith("/")) {
path += "/";
}

if (path.startsWith("/")) {
path = path.slice(1);
}

return url + path;
if (proxyUrl.length > 0 && !proxyUrl.endsWith("/")) {
proxyUrl += "/";
}

let url;
if (proxyUrl.length > 0 || proxyUrl === "/") {
let u = new URL(proxyUrl + "/api/webdav/" + path);
// add query params
u.searchParams.append("endpoint", config.endpoint);
url = u.toString();
} else {
url = "/api/upstash/" + path + "?endpoint=" + config.endpoint;
}
return url;
},
};
}
Loading

0 comments on commit a22141c

Please sign in to comment.