consolidate cross-origin proxy and add ssrf guard

This commit is contained in:
Nystik
2026-06-05 23:56:59 +02:00
parent b88f9fdc0e
commit 44bb01f162
5 changed files with 199 additions and 94 deletions

View File

@@ -1,6 +1,6 @@
import { showVaultManager } from "../ui-registry.js";
import { vaultService } from "@ignis/services";
import { arrayBufferToBase64, base64ToArrayBuffer } from "../util/base64.js";
import { proxyFetch } from "../util/proxy.js";
const listeners = new Map();
@@ -88,41 +88,19 @@ const syncHandlers = {
async function handleRequestUrl(requestId, request) {
try {
let body = request.body;
let binary = false;
if (body instanceof ArrayBuffer) {
body = arrayBufferToBase64(body);
binary = true;
}
const res = await fetch("/api/proxy", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
url: request.url,
method: request.method || "GET",
headers: request.headers || {},
contentType: request.contentType,
body,
binary,
}),
const result = await proxyFetch({
url: request.url,
method: request.method,
headers: request.headers,
body: request.body,
contentType: request.contentType,
});
const proxyResult = await res.json();
if (!res.ok) {
ipcRenderer._emit(requestId, {
error: proxyResult.error || "Proxy request failed",
});
return;
}
// Electron's e.reply(requestId, data) sends on the requestId channel
ipcRenderer._emit(requestId, {
status: proxyResult.status,
headers: proxyResult.headers,
body: base64ToArrayBuffer(proxyResult.body),
status: result.status,
headers: result.headers,
body: result.body,
});
} catch (e) {
ipcRenderer._emit(requestId, {

View File

@@ -4,8 +4,8 @@ import {
unregisterPopupWindow,
} from "./electron/remote/window.js";
import { showVaultManager } from "./ui-registry.js";
import { arrayBufferToBase64, base64ToArrayBuffer } from "./util/base64.js";
import { isSameOrigin } from "./util/url.js";
import { proxyFetch } from "./util/proxy.js";
function installProcess() {
window.process = processShim;
@@ -167,17 +167,15 @@ function installFetchShim() {
}
let body = null;
let binary = false;
if (init?.body && method !== "GET" && method !== "HEAD") {
if (typeof init.body === "string") {
body = init.body;
} else if (init.body instanceof ArrayBuffer) {
body = arrayBufferToBase64(init.body);
binary = true;
} else if (init.body instanceof Uint8Array) {
body = arrayBufferToBase64(init.body.buffer);
binary = true;
} else if (
init.body instanceof ArrayBuffer ||
init.body instanceof Uint8Array
) {
body = init.body;
} else if (typeof init.body === "object") {
body = JSON.stringify(init.body);
} else {
@@ -187,23 +185,15 @@ function installFetchShim() {
console.log("[shim:fetch] Proxying cross-origin:", method, url);
const proxyRes = await originalFetch("/api/proxy", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url, method, headers, body, binary }),
});
let result;
if (!proxyRes.ok) {
const err = await proxyRes
.json()
.catch(() => ({ error: "Proxy request failed" }));
throw new TypeError(err.error || "Failed to fetch");
try {
result = await proxyFetch({ url, method, headers, body });
} catch (e) {
throw new TypeError(e.message || "Failed to fetch");
}
const result = await proxyRes.json();
const respBody = base64ToArrayBuffer(result.body);
return new Response(respBody, {
return new Response(result.body, {
status: result.status,
headers: result.headers,
});

View File

@@ -2,7 +2,7 @@
// Obsidian sets window.requestUrl in app.js, so we override it after app.js loads.
import { isSameOrigin } from "./util/url.js";
import { arrayBufferToBase64, base64ToArrayBuffer } from "./util/base64.js";
import { proxyFetch } from "./util/proxy.js";
async function proxyRequestUrl(request) {
if (typeof request === "string") {
@@ -28,42 +28,14 @@ async function proxyRequestUrl(request) {
}
// Cross-origin: route through server proxy
let body = request.body;
let binary = false;
if (body instanceof ArrayBuffer) {
body = arrayBufferToBase64(body);
binary = true;
}
const res = await fetch("/api/proxy", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
url: request.url,
method: request.method || "GET",
headers: request.headers || {},
body,
binary,
}),
const result = await proxyFetch({
url: request.url,
method: request.method,
headers: request.headers,
body: request.body,
});
if (!res.ok) {
const err = await res
.json()
.catch(() => ({ error: "Proxy request failed" }));
throw new Error(err.error);
}
const proxyResult = await res.json();
const arrayBuf = base64ToArrayBuffer(proxyResult.body);
return makeResponse(
request,
proxyResult.status,
proxyResult.headers,
arrayBuf,
);
return makeResponse(request, result.status, result.headers, result.body);
}
function makeResponse(request, status, headers, arrayBuf) {

View File

@@ -0,0 +1,54 @@
// Single round-trip through the server's /api/proxy endpoint for cross-origin requests.
// Encodes a binary request body to base64, returns the upstream response with its body as an ArrayBuffer.
// Throws an Error carrying the server's message on failure.
import { arrayBufferToBase64, base64ToArrayBuffer } from "./base64.js";
export async function proxyFetch({ url, method, headers, body, contentType }) {
let encodedBody = null;
let binary = false;
if (body instanceof ArrayBuffer) {
encodedBody = arrayBufferToBase64(body);
binary = true;
} else if (body instanceof Uint8Array) {
encodedBody = arrayBufferToBase64(body.buffer);
binary = true;
} else if (body != null) {
encodedBody = body;
}
const payload = {
url,
method: method || "GET",
headers: headers || {},
body: encodedBody,
binary,
};
if (contentType !== undefined) {
payload.contentType = contentType;
}
// Use native fetch to avoid an unnecessary call through the shim. proxy is already same origin.
const nativeFetch = window.__originalFetch || fetch;
const res = await nativeFetch("/api/proxy", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error || "Proxy request failed");
}
const result = await res.json();
return {
status: result.status,
headers: result.headers,
body: base64ToArrayBuffer(result.body),
};
}