This commit is contained in:
+74
-13
@@ -8,10 +8,72 @@ import { detectAv } from '@file-type/av';
|
||||
import { config } from '../config.js';
|
||||
import { HttpError } from './errors.js';
|
||||
|
||||
function sanitizeFilename(value) {
|
||||
return String(value).replaceAll(/[^a-zA-Z0-9._-]/g, '_') || 'upload';
|
||||
}
|
||||
|
||||
function stripBase64Padding(value) {
|
||||
let end = value.length;
|
||||
while (end > 0 && value[end - 1] === '=') end -= 1;
|
||||
return value.slice(0, end);
|
||||
}
|
||||
|
||||
function base64FromDataUrl(value, fieldName) {
|
||||
const dataUrl = value.trim();
|
||||
const marker = ';base64,';
|
||||
const markerIndex = dataUrl.indexOf(marker);
|
||||
if (!dataUrl.startsWith('data:') || markerIndex < 5) {
|
||||
throw new HttpError(400, `${fieldName}.dataUrl muss eine Base64-Data-URL sein.`);
|
||||
}
|
||||
return dataUrl.slice(markerIndex + marker.length);
|
||||
}
|
||||
|
||||
function isXmlNameCharacter(character, first = false) {
|
||||
const code = character.codePointAt(0);
|
||||
const letter = (code >= 65 && code <= 90) || (code >= 97 && code <= 122);
|
||||
if (letter || character === '_') return true;
|
||||
return !first && ((code >= 48 && code <= 57) || character === '.' || character === '-' || character === ':');
|
||||
}
|
||||
|
||||
function rootElementName(xml) {
|
||||
let cursor = 0;
|
||||
while (cursor < xml.length) {
|
||||
const opening = xml.indexOf('<', cursor);
|
||||
if (opening < 0 || opening + 1 >= xml.length) return null;
|
||||
|
||||
if (xml.startsWith('<!--', opening)) {
|
||||
const end = xml.indexOf('-->', opening + 4);
|
||||
if (end < 0) return null;
|
||||
cursor = end + 3;
|
||||
continue;
|
||||
}
|
||||
if (xml.startsWith('<?', opening)) {
|
||||
const end = xml.indexOf('?>', opening + 2);
|
||||
if (end < 0) return null;
|
||||
cursor = end + 2;
|
||||
continue;
|
||||
}
|
||||
if (xml.startsWith('<!', opening)) {
|
||||
const end = xml.indexOf('>', opening + 2);
|
||||
if (end < 0) return null;
|
||||
cursor = end + 1;
|
||||
continue;
|
||||
}
|
||||
if (xml.startsWith('</', opening)) return null;
|
||||
|
||||
let end = opening + 1;
|
||||
if (!isXmlNameCharacter(xml[end], true)) return null;
|
||||
end += 1;
|
||||
while (end < xml.length && isXmlNameCharacter(xml[end])) end += 1;
|
||||
return xml.slice(opening + 1, end);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: path.join(config.storageDir, 'uploads'),
|
||||
filename(req, file, callback) {
|
||||
const safe = file.originalname.replace(/[^a-zA-Z0-9._-]/g, '_') || 'upload';
|
||||
const safe = sanitizeFilename(file.originalname);
|
||||
callback(null, `${Date.now()}-${crypto.randomUUID()}-${safe}`);
|
||||
}
|
||||
});
|
||||
@@ -41,14 +103,14 @@ function decodeBase64(value, fieldName) {
|
||||
throw new HttpError(400, `${fieldName}.base64 muss eine nichtleere Base64-Zeichenkette sein.`);
|
||||
}
|
||||
|
||||
const compact = value.replace(/\s+/g, '');
|
||||
const compact = value.replaceAll(/\s/g, '');
|
||||
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(compact) || compact.length % 4 === 1) {
|
||||
throw new HttpError(400, `${fieldName}.base64 enthält keine gültigen Base64-Daten.`);
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(compact, 'base64');
|
||||
const canonicalInput = compact.replace(/=+$/, '');
|
||||
const canonicalOutput = buffer.toString('base64').replace(/=+$/, '');
|
||||
const canonicalInput = stripBase64Padding(compact);
|
||||
const canonicalOutput = stripBase64Padding(buffer.toString('base64'));
|
||||
if (canonicalInput !== canonicalOutput) {
|
||||
throw new HttpError(400, `${fieldName}.base64 enthält keine gültigen Base64-Daten.`);
|
||||
}
|
||||
@@ -71,11 +133,7 @@ function jsonFileDescriptor(value, fieldName) {
|
||||
if (typeof value.dataUrl !== 'string') {
|
||||
throw new HttpError(400, `${fieldName}.dataUrl muss eine Zeichenkette sein.`);
|
||||
}
|
||||
const match = /^data:[^,]*;base64,(.*)$/s.exec(value.dataUrl.trim());
|
||||
if (!match) {
|
||||
throw new HttpError(400, `${fieldName}.dataUrl muss eine Base64-Data-URL sein.`);
|
||||
}
|
||||
base64 = match[1];
|
||||
base64 = base64FromDataUrl(value.dataUrl, fieldName);
|
||||
}
|
||||
|
||||
return { originalname, buffer: decodeBase64(base64, fieldName) };
|
||||
@@ -89,7 +147,7 @@ function materializeJsonUpload(req, fieldName) {
|
||||
|
||||
const destination = path.join(config.storageDir, 'uploads');
|
||||
fs.mkdirSync(destination, { recursive: true });
|
||||
const safe = descriptor.originalname.replace(/[^a-zA-Z0-9._-]/g, '_') || 'upload';
|
||||
const safe = sanitizeFilename(descriptor.originalname);
|
||||
const filename = `${Date.now()}-${crypto.randomUUID()}-${safe}`;
|
||||
const filePath = path.join(destination, filename);
|
||||
fs.writeFileSync(filePath, descriptor.buffer, { flag: 'wx' });
|
||||
@@ -116,8 +174,11 @@ function detectGpx(file) {
|
||||
throw new HttpError(415, 'Der Upload ist keine gültig kodierte GPX-Datei.');
|
||||
}
|
||||
|
||||
const start = xml.slice(0, 131072).replace(/^\uFEFF/, '');
|
||||
if (!/<(?:[A-Za-z_][\w.-]*:)?gpx(?:\s|>)/i.test(start)) {
|
||||
const prefix = xml.slice(0, 131072);
|
||||
const start = prefix.startsWith('\uFEFF') ? prefix.slice(1) : prefix;
|
||||
const rootName = rootElementName(start);
|
||||
const localName = rootName?.split(':').at(-1);
|
||||
if (localName !== 'gpx') {
|
||||
throw new HttpError(415, 'Der Upload enthält kein GPX-Dokument.');
|
||||
}
|
||||
return { ext: 'gpx', mime: 'application/gpx+xml' };
|
||||
@@ -146,7 +207,7 @@ async function detectAndValidate(file, kind) {
|
||||
const rules = ALLOWED_TYPES[kind];
|
||||
const baseMime = String(detected.mime).split(';', 1)[0].toLowerCase();
|
||||
const extension = String(detected.ext).toLowerCase();
|
||||
if (!rules || !rules.mime.has(baseMime) || !rules.ext.has(extension)) {
|
||||
if (!rules?.mime.has(baseMime) || !rules?.ext.has(extension)) {
|
||||
throw new HttpError(415, kind === 'picture'
|
||||
? `Nicht unterstütztes Bildformat: ${detected.mime}`
|
||||
: `Nicht unterstütztes Audioformat: ${detected.mime}`);
|
||||
|
||||
Reference in New Issue
Block a user