From 0e0da6d722f656c9d994e51543edc08940e06740 Mon Sep 17 00:00:00 2001 From: "Emma [it/its]@Rory&" Date: Wed, 2 Jul 2025 16:40:25 +0200 Subject: [PATCH] Add options to erquire IP or User Agent to access CDN --- src/api/routes/attachments/refresh-urls.ts | 2 +- .../channels/#channel_id/messages/index.ts | 4 +- src/cdn/routes/attachments.ts | 4 +- src/util/Signing.ts | 48 ++++++++++++------- src/util/config/types/CdnConfiguration.ts | 4 -- .../config/types/SecurityConfiguration.ts | 2 + 6 files changed, 38 insertions(+), 26 deletions(-) diff --git a/src/api/routes/attachments/refresh-urls.ts b/src/api/routes/attachments/refresh-urls.ts index e456a911..78f6b7fc 100644 --- a/src/api/routes/attachments/refresh-urls.ts +++ b/src/api/routes/attachments/refresh-urls.ts @@ -37,7 +37,7 @@ router.post( async (req: Request, res: Response) => { const { attachment_urls } = req.body as RefreshUrlsRequestSchema; - const refreshed_urls = attachment_urls.map(resignUrl); + const refreshed_urls = attachment_urls.map(url => resignUrl(url, req)); return res.status(200).json({ refreshed_urls, diff --git a/src/api/routes/channels/#channel_id/messages/index.ts b/src/api/routes/channels/#channel_id/messages/index.ts index a540dce6..3e0a056e 100644 --- a/src/api/routes/channels/#channel_id/messages/index.ts +++ b/src/api/routes/channels/#channel_id/messages/index.ts @@ -210,8 +210,8 @@ router.get( y.proxy_url = url.toString(); - y.proxy_url = resignUrl(y.proxy_url); - y.url = resignUrl(y.url); + y.proxy_url = resignUrl(y.proxy_url, req); + y.url = resignUrl(y.url, req); }); /** diff --git a/src/cdn/routes/attachments.ts b/src/cdn/routes/attachments.ts index 2b788a11..a725d508 100644 --- a/src/cdn/routes/attachments.ts +++ b/src/cdn/routes/attachments.ts @@ -72,7 +72,7 @@ router.post( let finalUrl = `${endpoint}/${path}`; if (Config.get().security.cdnSignUrls) { - const signatureData = getUrlSignature(path); + const signatureData = getUrlSignature(path, req); finalUrl = `${finalUrl}?ex=${signatureData.expiresAt}&is=${signatureData.issuedAt}&hm=${signatureData.hash}`; } @@ -100,7 +100,7 @@ router.get( if ( Config.get().security.cdnSignUrls && - !hasValidSignature(path, req.query) + !hasValidSignature(path, req.query, req) ) { return res.status(404).send("This content is no longer available."); } diff --git a/src/util/Signing.ts b/src/util/Signing.ts index 5763ed16..fa3b8786 100644 --- a/src/util/Signing.ts +++ b/src/util/Signing.ts @@ -20,8 +20,12 @@ import { Config } from "@spacebar/util"; import { createHmac, timingSafeEqual } from "crypto"; import ms, { StringValue } from "ms"; import { ParsedQs } from "qs"; +import { Request } from "express"; -export const getUrlSignature = (path: string) => { +export const getUrlSignature = ( + path: string, + req: Request, +) => { const { cdnSignatureKey, cdnSignatureDuration } = Config.get().security; // calculate the expiration time @@ -32,11 +36,7 @@ export const getUrlSignature = (path: string) => { ); // hash the url with the cdnSignatureKey - const hash = createHmac("sha256", cdnSignatureKey as string) - .update(path) - .update(issuedAt) - .update(expiresAt) - .digest("hex"); + const hash = calculateHash(path, issuedAt, expiresAt, req); return { hash, @@ -49,14 +49,31 @@ export const calculateHash = ( url: string, issuedAt: string, expiresAt: string, + req: Request, ) => { const { cdnSignatureKey } = Config.get().security; const hash = createHmac("sha256", cdnSignatureKey as string) .update(url) .update(issuedAt) - .update(expiresAt) - .digest("hex"); - return hash; + .update(expiresAt); + + if (Config.get().security.cdnSignatureIncludeIp) { + if (!req || !req.ip) + console.log( + "[Signing] CDN Signature IP is enabled but no request object was provided. This may cause issues with signature validation. Please report this to the Spacebar team!", + ); + hash.update(req.ip!); + } + + if (Config.get().security.cdnSignatureIncludeUserAgent) { + if (!req || !req.headers || !req.headers["user-agent"]) + console.log( + "[Signing] CDN Signature User-Agent is enabled but no request object was provided. This may cause issues with signature validation. Please report this to the Spacebar team!", + ); + hash.update(req.headers["user-agent"] as string); + } + + return hash.digest("hex"); }; export const isExpired = (ex: string, is: string) => { @@ -80,7 +97,7 @@ export const isExpired = (ex: string, is: string) => { return false; }; -export const hasValidSignature = (path: string, query: ParsedQs) => { +export const hasValidSignature = (path: string, query: ParsedQs, req: Request) => { // get url path const { ex, is, hm } = query; @@ -92,21 +109,18 @@ export const hasValidSignature = (path: string, query: ParsedQs) => { return false; } - const calcd = calculateHash(path, is as string, ex as string); + const calcd = calculateHash(path, is as string, ex as string, req); const calculated = Buffer.from(calcd); const received = Buffer.from(hm as string); const isHashValid = calculated.length === received.length && timingSafeEqual(calculated, received); - // if (!isHashValid) { - // console.debug("Invalid signature"); - // console.debug(calcd, hm); - // } + return isHashValid; }; -export const resignUrl = (attachmentUrl: string) => { +export const resignUrl = (attachmentUrl: string, req: Request) => { const url = new URL(attachmentUrl); // if theres an existing signature, check if its expired or not. no reason to resign if its not expired @@ -127,7 +141,7 @@ export const resignUrl = (attachmentUrl: string) => { path = path.slice(1); } - const { hash, issuedAt, expiresAt } = getUrlSignature(path); + const { hash, issuedAt, expiresAt } = getUrlSignature(path, req); url.searchParams.set("ex", expiresAt); url.searchParams.set("is", issuedAt); url.searchParams.set("hm", hash); diff --git a/src/util/config/types/CdnConfiguration.ts b/src/util/config/types/CdnConfiguration.ts index 842cb87c..b0121c37 100644 --- a/src/util/config/types/CdnConfiguration.ts +++ b/src/util/config/types/CdnConfiguration.ts @@ -22,9 +22,5 @@ export class CdnConfiguration extends EndpointConfiguration { resizeHeightMax: number = 1000; resizeWidthMax: number = 1000; imagorServerUrl: string | null = null; - - endpointPublic: string | null = null; - endpointPrivate: string | null = null; - proxyCacheHeaderSeconds: number = 60 * 60 * 24; } diff --git a/src/util/config/types/SecurityConfiguration.ts b/src/util/config/types/SecurityConfiguration.ts index fb8b550e..cb18c96a 100644 --- a/src/util/config/types/SecurityConfiguration.ts +++ b/src/util/config/types/SecurityConfiguration.ts @@ -41,4 +41,6 @@ export class SecurityConfiguration { cdnSignUrls: boolean = false; cdnSignatureKey: string = crypto.randomBytes(32).toString("base64"); cdnSignatureDuration: string = "24h"; + cdnSignatureIncludeIp: boolean = true; + cdnSignatureIncludeUserAgent: boolean = true; }