Something went wrong. Try again.
Mirror of https://github.com/improsocial/improAn extensible Bluesky client for web
Something went wrong. Try again.
JavaScript
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192import { isFetchAllowed } from "/js/plugins/pluginPermissions.js";
const ALLOWED_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"];
const FORBIDDEN_HEADERS = ["authorization", "cookie"];const MAX_BODY_CHARS = 1_000_000;
export async function pluginFetch( plugin, url, init, fetchImpl = fetch.bind(globalThis),) { if (!isFetchAllowed(url, plugin.permissions)) { throw new Error(`fetch to "${url}" not permitted`); } return sandboxedFetch(url, init, { forbiddenHeaders: FORBIDDEN_HEADERS, maxBodyChars: MAX_BODY_CHARS, fetchImpl, });}
// Shared hardening for any plugin-initiated fetch, regardless of which// permission model authorized the destination URL: method allowlist, a// caller-supplied forbidden-header set, a body-size cap, and the same// credential/redirect/referrer lockdown — only content-type is ever handed// back from the response. `forbiddenHeaders` differs by caller: ordinary// plugin fetch (pluginFetch, above) forbids Authorization (it could only// ever be forwarding an ambient credential the plugin has no business// touching) and Cookie; the user-approved single-endpoint path// (pluginConfiguredFetch.js) allows Authorization through, since there the// header value is the plugin's own credential for a URL a human explicitly// approved, not anything ambient.export async function sandboxedFetch( url, init, { forbiddenHeaders, maxBodyChars, fetchImpl = fetch.bind(globalThis) },) { const response = await fetchImpl(url, { ...sanitizeFetchInit(init, { forbiddenHeaders, maxBodyChars }), credentials: "omit", redirect: "error", mode: "cors", referrerPolicy: "no-referrer", }); const bodyText = await response.text(); return { status: response.status, ok: response.ok, headers: filterResponseHeaders(response.headers, ["content-type"]), body: bodyText, };}
export function sanitizeFetchInit(init, { forbiddenHeaders, maxBodyChars }) { const safeInit = {}; const method = (init?.method ?? "GET").toUpperCase(); if (!ALLOWED_METHODS.includes(method)) { throw new Error(`fetch method "${method}" not permitted`); } safeInit.method = method; const headers = {}; for (const [name, value] of Object.entries(init?.headers ?? {})) { const lowerName = String(name).toLowerCase(); if (forbiddenHeaders.includes(lowerName)) { throw new Error(`fetch header "${name}" not permitted`); } headers[name] = String(value); } safeInit.headers = headers; if (init?.body != null) { if (typeof init.body !== "string") { throw new Error("fetch body must be a string"); } if (init.body.length > maxBodyChars) { throw new Error("fetch body too large"); } safeInit.body = init.body; } return safeInit;}
function filterResponseHeaders(headers, allowedNames) { const picked = {}; for (const name of allowedNames) { const value = headers.get(name); if (value != null) picked[name] = value; } return picked;}