Allow arbitrary issue attachments

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta 2026-04-05 06:29:33 -05:00
parent 517fe5093e
commit e9c8bd4805
5 changed files with 242 additions and 13 deletions

View file

@ -47,7 +47,12 @@ import { logger } from "../middleware/logger.js";
import { forbidden, HttpError, unauthorized } from "../errors.js";
import { assertCompanyAccess, getActorInfo } from "./authz.js";
import { shouldWakeAssigneeOnCheckout } from "./issues-checkout-wakeup.js";
import { isAllowedContentType, MAX_ATTACHMENT_BYTES } from "../attachment-types.js";
import {
isInlineAttachmentContentType,
MAX_ATTACHMENT_BYTES,
normalizeContentType,
SVG_CONTENT_TYPE,
} from "../attachment-types.js";
import { queueIssueAssignmentWakeup } from "../services/issue-assignment-wakeup.js";
const MAX_ISSUE_COMMENT_LIMIT = 500;
@ -2108,11 +2113,7 @@ export function issueRoutes(
res.status(400).json({ error: "Missing file field 'file'" });
return;
}
const contentType = (file.mimetype || "").toLowerCase();
if (!isAllowedContentType(contentType)) {
res.status(422).json({ error: `Unsupported attachment type: ${contentType || "unknown"}` });
return;
}
const contentType = normalizeContentType(file.mimetype);
if (file.buffer.length <= 0) {
res.status(422).json({ error: "Attachment is empty" });
return;
@ -2176,11 +2177,17 @@ export function issueRoutes(
assertCompanyAccess(req, attachment.companyId);
const object = await storage.getObject(attachment.companyId, attachment.objectKey);
res.setHeader("Content-Type", attachment.contentType || object.contentType || "application/octet-stream");
const responseContentType = normalizeContentType(attachment.contentType || object.contentType);
res.setHeader("Content-Type", responseContentType);
res.setHeader("Content-Length", String(attachment.byteSize || object.contentLength || 0));
res.setHeader("Cache-Control", "private, max-age=60");
res.setHeader("X-Content-Type-Options", "nosniff");
if (responseContentType === SVG_CONTENT_TYPE) {
res.setHeader("Content-Security-Policy", "sandbox; default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'");
}
const filename = attachment.originalFilename ?? "attachment";
res.setHeader("Content-Disposition", `inline; filename=\"${filename.replaceAll("\"", "")}\"`);
const disposition = isInlineAttachmentContentType(responseContentType) ? "inline" : "attachment";
res.setHeader("Content-Disposition", `${disposition}; filename=\"${filename.replaceAll("\"", "")}\"`);
object.stream.on("error", (err) => {
next(err);