fix(upload): sniff magic bytes when CDN serves generic binary/octet-stream
Publish container image / build-and-push (push) Canceled after 0s
Publish container image / build-and-push (push) Canceled after 0s
This commit is contained in:
@@ -307,9 +307,11 @@ function downloadImage(imageUrl, maxRedirects = 5) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const contentType = (response.headers['content-type'] || '').split(';')[0].trim();
|
const contentType = (response.headers['content-type'] || '').split(';')[0].trim();
|
||||||
if (!ALLOWED_MIME_TYPES.includes(contentType)) {
|
const GENERIC_TYPES = ['binary/octet-stream', 'application/octet-stream'];
|
||||||
|
if (!ALLOWED_MIME_TYPES.includes(contentType) && !GENERIC_TYPES.includes(contentType)) {
|
||||||
return reject(new Error(`Unsupported content type: ${contentType}`));
|
return reject(new Error(`Unsupported content type: ${contentType}`));
|
||||||
}
|
}
|
||||||
|
const typeIsGeneric = GENERIC_TYPES.includes(contentType);
|
||||||
|
|
||||||
const chunks = [];
|
const chunks = [];
|
||||||
let totalSize = 0;
|
let totalSize = 0;
|
||||||
@@ -324,6 +326,12 @@ function downloadImage(imageUrl, maxRedirects = 5) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
response.on('end', () => {
|
response.on('end', () => {
|
||||||
|
if (typeIsGeneric) {
|
||||||
|
const sniffed = sniffMime(Buffer.concat(chunks));
|
||||||
|
if (!sniffed) {
|
||||||
|
return reject(new Error('Unsupported content type: binary/octet-stream (unknown magic bytes)'));
|
||||||
|
}
|
||||||
|
}
|
||||||
const buffer = Buffer.concat(chunks);
|
const buffer = Buffer.concat(chunks);
|
||||||
const filename = parsed.pathname.split('/').pop() || 'image';
|
const filename = parsed.pathname.split('/').pop() || 'image';
|
||||||
resolve({ buffer, mimeType: contentType, filename });
|
resolve({ buffer, mimeType: contentType, filename });
|
||||||
@@ -425,4 +433,24 @@ router.use((err, req, res, next) => {
|
|||||||
next(err);
|
next(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Sniff the real MIME type from magic bytes (for CDNs that serve everything
|
||||||
|
// as binary/octet-stream). Returns true when the buffer looks like an
|
||||||
|
// allowed image/video/pdf type.
|
||||||
|
function sniffMime(buf) {
|
||||||
|
if (!buf || buf.length < 12) return false;
|
||||||
|
const hex = buf.slice(0, 12).toString('hex');
|
||||||
|
const ascii = buf.slice(0, 12).toString('latin1');
|
||||||
|
if (hex.startsWith('89504e47')) return true; // PNG
|
||||||
|
if (hex.startsWith('ffd8ff')) return true; // JPEG
|
||||||
|
if (ascii.startsWith('GIF87a') || ascii.startsWith('GIF89a')) return true; // GIF
|
||||||
|
if (ascii.startsWith('RIFF') && buf.slice(8, 12).toString('latin1') === 'WEBP') return true; // WebP
|
||||||
|
if (ascii.startsWith('%PDF')) return true; // PDF
|
||||||
|
if (ascii.startsWith('ftyp')) return true; // MP4/QuickTime (ftyp at 0 for some, 4 for others)
|
||||||
|
if (buf.slice(4, 8).toString('latin1') === 'ftyp') return true;
|
||||||
|
if (hex.startsWith('1a45dfa3')) return true; // WebM/Matroska
|
||||||
|
if (ascii.startsWith('OggS')) return false; // ogg not allowed
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
Reference in New Issue
Block a user