This commit is contained in:
Rory& 2025-07-06 11:08:51 +02:00
parent 199a518092
commit b590482bfb
9 changed files with 148 additions and 75 deletions

View File

@ -17,7 +17,11 @@
*/ */
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { RefreshUrlsRequestSchema, getUrlSignature, NewUrlSignatureData } from "@spacebar/util"; import {
RefreshUrlsRequestSchema,
getUrlSignature,
NewUrlSignatureData,
} from "@spacebar/util";
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
@ -37,12 +41,16 @@ router.post(
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { attachment_urls } = req.body as RefreshUrlsRequestSchema; const { attachment_urls } = req.body as RefreshUrlsRequestSchema;
const refreshed_urls = attachment_urls.map(url => { const refreshed_urls = attachment_urls.map((url) => {
return getUrlSignature(new NewUrlSignatureData({ return getUrlSignature(
url: url, new NewUrlSignatureData({
ip: req.ip, url: url,
userAgent: req.headers["user-agent"] as string ip: req.ip,
})).applyToUrl(url).toString(); userAgent: req.headers["user-agent"] as string,
}),
)
.applyToUrl(url)
.toString();
}); });
return res.status(200).json({ return res.status(200).json({

View File

@ -30,7 +30,8 @@ import {
emitEvent, emitEvent,
getPermission, getPermission,
getRights, getRights,
uploadFile, NewUrlUserSignatureData, uploadFile,
NewUrlUserSignatureData,
} from "@spacebar/util"; } from "@spacebar/util";
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
@ -245,10 +246,14 @@ router.put(
console.error("[Message] post-message handler failed", e), console.error("[Message] post-message handler failed", e),
); );
return res.json(message.withSignedAttachments(new NewUrlUserSignatureData({ return res.json(
ip: req.ip, message.withSignedAttachments(
userAgent: req.headers["user-agent"] as string, new NewUrlUserSignatureData({
}))); ip: req.ip,
userAgent: req.headers["user-agent"] as string,
}),
),
);
}, },
); );

View File

@ -36,7 +36,9 @@ import {
getPermission, getPermission,
isTextChannel, isTextChannel,
getUrlSignature, getUrlSignature,
uploadFile, NewUrlSignatureData, NewUrlUserSignatureData, uploadFile,
NewUrlSignatureData,
NewUrlUserSignatureData,
} from "@spacebar/util"; } from "@spacebar/util";
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
@ -210,17 +212,25 @@ router.get(
y.proxy_url = url.toString(); y.proxy_url = url.toString();
y.proxy_url = getUrlSignature(new NewUrlSignatureData({ y.proxy_url = getUrlSignature(
url: y.proxy_url, new NewUrlSignatureData({
userAgent: req.headers["user-agent"], url: y.proxy_url,
ip: req.ip, userAgent: req.headers["user-agent"],
})).applyToUrl(y.proxy_url).toString(); ip: req.ip,
}),
)
.applyToUrl(y.proxy_url)
.toString();
y.url = getUrlSignature(new NewUrlSignatureData({ y.url = getUrlSignature(
url: y.url, new NewUrlSignatureData({
userAgent: req.headers["user-agent"], url: y.url,
ip: req.ip, userAgent: req.headers["user-agent"],
})).applyToUrl(y.url).toString(); ip: req.ip,
}),
)
.applyToUrl(y.url)
.toString();
}); });
/** /**
@ -439,10 +449,14 @@ router.post(
console.error("[Message] post-message handler failed", e), console.error("[Message] post-message handler failed", e),
); );
return res.json(message.withSignedAttachments(new NewUrlUserSignatureData({ return res.json(
ip: req.ip, message.withSignedAttachments(
userAgent: req.headers["user-agent"] as string, new NewUrlUserSignatureData({
}))); ip: req.ip,
userAgent: req.headers["user-agent"] as string,
}),
),
);
}, },
); );

View File

@ -18,8 +18,10 @@
import { import {
Config, Config,
hasValidSignature, NewUrlUserSignatureData, hasValidSignature,
Snowflake, UrlSignResult, NewUrlUserSignatureData,
Snowflake,
UrlSignResult,
} from "@spacebar/util"; } from "@spacebar/util";
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
import FileType from "file-type"; import FileType from "file-type";
@ -93,16 +95,21 @@ router.get(
const path = `attachments/${channel_id}/${id}/${filename}`; const path = `attachments/${channel_id}/${id}/${filename}`;
const fullUrl = (req.headers["x-forwarded-proto"] ?? req.protocol) + "://" const fullUrl =
+ (req.headers["x-forwarded-host"] ?? req.hostname) (req.headers["x-forwarded-proto"] ?? req.protocol) +
+ req.originalUrl; "://" +
(req.headers["x-forwarded-host"] ?? req.hostname) +
req.originalUrl;
if ( if (
Config.get().security.cdnSignUrls && Config.get().security.cdnSignUrls &&
!hasValidSignature(new NewUrlUserSignatureData({ !hasValidSignature(
ip: req.ip, new NewUrlUserSignatureData({
userAgent: req.headers["user-agent"] as string, ip: req.ip,
}), UrlSignResult.fromUrl(fullUrl)) userAgent: req.headers["user-agent"] as string,
}),
UrlSignResult.fromUrl(fullUrl),
)
) { ) {
return res.status(404).send("This content is no longer available."); return res.status(404).send("This content is no longer available.");
} }

View File

@ -27,7 +27,8 @@ import {
EVENTEnum, EVENTEnum,
Relationship, Relationship,
RelationshipType, RelationshipType,
Message, NewUrlUserSignatureData, Message,
NewUrlUserSignatureData,
} from "@spacebar/util"; } from "@spacebar/util";
import { OPCODES } from "../util/Constants"; import { OPCODES } from "../util/Constants";
import { Send } from "../util/Send"; import { Send } from "../util/Send";
@ -291,11 +292,15 @@ async function consume(this: WebSocket, opts: EventOpts) {
case "MESSAGE_CREATE": case "MESSAGE_CREATE":
case "MESSAGE_UPDATE": case "MESSAGE_UPDATE":
// console.log(this.request) // console.log(this.request)
if(data["attachments"]) if (data["attachments"])
data["attachments"] = Message.prototype.withSignedAttachments.call(data, new NewUrlUserSignatureData({ data["attachments"] =
ip: this.ipAddress, Message.prototype.withSignedAttachments.call(
userAgent: this.userAgent data,
})).attachments; new NewUrlUserSignatureData({
ip: this.ipAddress,
userAgent: this.userAgent,
}),
).attachments;
break; break;
default: default:
break; break;

View File

@ -39,7 +39,9 @@ export class NewUrlSignatureData extends NewUrlUserSignatureData {
this.path = data.path; this.path = data.path;
this.url = data.url; this.url = data.url;
if (!this.path && !this.url) { if (!this.path && !this.url) {
throw new Error("Either path or url must be provided for URL signing"); throw new Error(
"Either path or url must be provided for URL signing",
);
} }
if (this.path && this.url) { if (this.path && this.url) {
console.warn( console.warn(
@ -51,7 +53,9 @@ export class NewUrlSignatureData extends NewUrlUserSignatureData {
const parsedUrl = new URL(this.url); const parsedUrl = new URL(this.url);
this.path = parsedUrl.pathname; this.path = parsedUrl.pathname;
} catch (e) { } catch (e) {
throw new Error("Invalid URL provided for signing: " + this.url); throw new Error(
"Invalid URL provided for signing: " + this.url,
);
} }
} }
} }
@ -176,12 +180,21 @@ function calculateHash(request: UrlSignatureData): UrlSignResult {
expiresAt: request.expiresAt, expiresAt: request.expiresAt,
hash, hash,
}); });
console.log("[Signing]", { console.log(
path: request.path, "[Signing]",
validity: request.issuedAt + " .. " + request.expiresAt, {
ua: Config.get().security.cdnSignatureIncludeUserAgent ? request.userAgent : "[disabled]", path: request.path,
ip: Config.get().security.cdnSignatureIncludeIp ? request.ip : "[disabled]" validity: request.issuedAt + " .. " + request.expiresAt,
}, "->", result); ua: Config.get().security.cdnSignatureIncludeUserAgent
? request.userAgent
: "[disabled]",
ip: Config.get().security.cdnSignatureIncludeIp
? request.ip
: "[disabled]",
},
"->",
result,
);
return result; return result;
} }
@ -212,7 +225,10 @@ export const isExpired = (data: UrlSignResult | UrlSignatureData) => {
return false; return false;
}; };
export const hasValidSignature = (req: NewUrlUserSignatureData, sig: UrlSignResult) => { export const hasValidSignature = (
req: NewUrlUserSignatureData,
sig: UrlSignResult,
) => {
// if the required query parameters are not present, return false // if the required query parameters are not present, return false
if (!sig.expiresAt || !sig.issuedAt || !sig.hash) { if (!sig.expiresAt || !sig.issuedAt || !sig.hash) {
console.warn( console.warn(
@ -227,31 +243,45 @@ export const hasValidSignature = (req: NewUrlUserSignatureData, sig: UrlSignResu
return false; return false;
} }
const calcResult = calculateHash(new UrlSignatureData({ const calcResult = calculateHash(
path: sig.path, new UrlSignatureData({
issuedAt: sig.issuedAt, path: sig.path,
expiresAt: sig.expiresAt, issuedAt: sig.issuedAt,
ip: req.ip, expiresAt: sig.expiresAt,
userAgent: req.userAgent ip: req.ip,
})); userAgent: req.userAgent,
}),
);
const calcd = calcResult.hash; const calcd = calcResult.hash;
const calculated = Buffer.from(calcd); const calculated = Buffer.from(calcd);
const received = Buffer.from(sig.hash as string); const received = Buffer.from(sig.hash as string);
console.assert(sig.issuedAt == calcResult.issuedAt, "[Signing] Mismatched issuedAt", { console.assert(
is: sig.issuedAt, sig.issuedAt == calcResult.issuedAt,
calculated: calcResult.issuedAt, "[Signing] Mismatched issuedAt",
}); {
is: sig.issuedAt,
calculated: calcResult.issuedAt,
},
);
console.assert(sig.expiresAt == calcResult.expiresAt, "[Signing] Mismatched expiresAt", { console.assert(
ex: sig.expiresAt, sig.expiresAt == calcResult.expiresAt,
calculated: calcResult.expiresAt, "[Signing] Mismatched expiresAt",
}); {
ex: sig.expiresAt,
calculated: calcResult.expiresAt,
},
);
console.assert(calculated.length === received.length, "[Signing] Mismatched hash length", { console.assert(
calculated: calculated.length, calculated.length === received.length,
received: received.length, "[Signing] Mismatched hash length",
}); {
calculated: calculated.length,
received: received.length,
},
);
const isHashValid = const isHashValid =
calculated.length === received.length && calculated.length === received.length &&

View File

@ -84,10 +84,14 @@ export class Attachment extends BaseClass {
...this, ...this,
url: getUrlSignature( url: getUrlSignature(
new NewUrlSignatureData({ ...data, url: this.url }), new NewUrlSignatureData({ ...data, url: this.url }),
).applyToUrl(this.url).toString(), )
.applyToUrl(this.url)
.toString(),
proxy_url: getUrlSignature( proxy_url: getUrlSignature(
new NewUrlSignatureData({ ...data, url: this.proxy_url }), new NewUrlSignatureData({ ...data, url: this.proxy_url }),
).applyToUrl(this.proxy_url).toString(), )
.applyToUrl(this.proxy_url)
.toString(),
}; };
} }
} }

View File

@ -266,8 +266,8 @@ export class Message extends BaseClass {
return { return {
...this, ...this,
attachments: this.attachments?.map((attachment: Attachment) => attachments: this.attachments?.map((attachment: Attachment) =>
Attachment.prototype.signUrls.call(attachment, data) Attachment.prototype.signUrls.call(attachment, data),
) ),
}; };
} }
} }

View File

@ -25,7 +25,7 @@ const jwtSignOptions: jwt.SignOptions = {
expiresIn: "5m", expiresIn: "5m",
}; };
const jwtVerifyOptions: jwt.VerifyOptions = { const jwtVerifyOptions: jwt.VerifyOptions = {
algorithms: ["HS256"] algorithms: ["HS256"],
}; };
export const WebAuthn: { export const WebAuthn: {