mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-08 20:57:08 +08:00
feat: improve attachment headers (#13709)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Safer, consistent file downloads with automatic attachment headers and filenames. - Smarter MIME detection for uploads (avatars, workspace blobs, Copilot files/transcripts). - Sensible default buffer limit when reading uploads. - **Bug Fixes** - Prevents risky content from rendering inline by forcing downloads and adding no‑sniff protection. - More accurate content types when original metadata is missing or incorrect. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -135,4 +135,4 @@ export const StorageJSONSchema: JSONSchema = {
|
||||
};
|
||||
|
||||
export type * from './provider';
|
||||
export { autoMetadata, toBuffer } from './utils';
|
||||
export { applyAttachHeaders, autoMetadata, sniffMime, toBuffer } from './utils';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Readable } from 'node:stream';
|
||||
|
||||
import { crc32 } from '@node-rs/crc32';
|
||||
import type { Response } from 'express';
|
||||
import { getStreamAsBuffer } from 'get-stream';
|
||||
|
||||
import { getMime } from '../../../native';
|
||||
@@ -43,4 +44,53 @@ export function autoMetadata(
|
||||
return metadata;
|
||||
}
|
||||
|
||||
const DANGEROUS_INLINE_MIME_PREFIXES = [
|
||||
'text/html',
|
||||
'application/xhtml+xml',
|
||||
'image/svg+xml',
|
||||
'application/xml',
|
||||
'text/xml',
|
||||
'text/javascript',
|
||||
];
|
||||
|
||||
export function isDangerousInlineMime(mime: string | undefined) {
|
||||
if (!mime) return false;
|
||||
const lower = mime.toLowerCase();
|
||||
return DANGEROUS_INLINE_MIME_PREFIXES.some(p => lower.startsWith(p));
|
||||
}
|
||||
|
||||
export function applyAttachHeaders(
|
||||
res: Response,
|
||||
options: { filename?: string; buffer?: Buffer; contentType?: string }
|
||||
) {
|
||||
let { filename, buffer, contentType } = options;
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||
if (!contentType && buffer) contentType = sniffMime(buffer);
|
||||
if (contentType && isDangerousInlineMime(contentType)) {
|
||||
const safeName = (filename || 'download')
|
||||
.replace(/[\r\n]/g, '')
|
||||
.replace(/[^\w\s.-]/g, '_');
|
||||
res.setHeader(
|
||||
'Content-Disposition',
|
||||
`attachment; filename="${encodeURIComponent(safeName)}"; filename*=UTF-8''${encodeURIComponent(
|
||||
safeName
|
||||
)}`
|
||||
);
|
||||
}
|
||||
if (!res.getHeader('Content-Type')) {
|
||||
res.setHeader('Content-Type', contentType || 'application/octet-stream');
|
||||
}
|
||||
}
|
||||
|
||||
export function sniffMime(
|
||||
buffer: Buffer,
|
||||
declared?: string
|
||||
): string | undefined {
|
||||
try {
|
||||
const detected = getMime(buffer);
|
||||
if (detected) return detected;
|
||||
} catch {}
|
||||
return declared;
|
||||
}
|
||||
|
||||
export const SIGNED_URL_EXPIRED = 60 * 60; // 1 hour
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Readable } from 'node:stream';
|
||||
|
||||
import { BlobQuotaExceeded, StorageQuotaExceeded } from '../error';
|
||||
import { OneKB } from './unit';
|
||||
|
||||
export type CheckExceededResult =
|
||||
| {
|
||||
@@ -52,7 +53,7 @@ export async function readBuffer(
|
||||
|
||||
export async function readBufferWithLimit(
|
||||
readable: Readable,
|
||||
limit: number
|
||||
limit: number = 500 * OneKB
|
||||
): Promise<Buffer> {
|
||||
return readBuffer(readable, size =>
|
||||
size > limit
|
||||
|
||||
Reference in New Issue
Block a user