feat: implement secure image upload pipeline and backend XSS guard (Stage 2 Task A)

This commit is contained in:
2026-06-11 20:32:05 +02:00
parent ec3fc52a73
commit 155bfa9aa0
10 changed files with 242 additions and 40 deletions
@@ -50,12 +50,11 @@ export async function initEditor(elementId, dotNetHelper, initialMarkdown) {
[Crepe.Feature.ImageBlock]: {
onUpload: async (file) => {
try {
const arrayBuffer = await file.arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
const url = await dotNetHelper.invokeMethodAsync('UploadImageFromJs', file.name, file.type, uint8Array);
const streamRef = DotNet.createJSStreamReference(file);
const url = await dotNetHelper.invokeMethodAsync('UploadImageFromJs', file.name, file.type, streamRef);
return url;
} catch (err) {
console.error("[Milkdown] Failed to upload image from JS:", err);
console.error("[Milkdown] Failed to upload image from JS (onUpload):", err);
throw err;
}
}
@@ -63,6 +62,36 @@ export async function initEditor(elementId, dotNetHelper, initialMarkdown) {
}
});
// Configure custom uploader using the uploadConfig context slice
crepe.editor.config((ctx) => {
try {
ctx.update('uploadConfig', (prev) => ({
...prev,
uploader: async (files, schema) => {
const nodes = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (file.type.startsWith('image/')) {
try {
const streamRef = DotNet.createJSStreamReference(file);
const uploadedUrl = await dotNetHelper.invokeMethodAsync('UploadImageFromJs', file.name, file.type, streamRef);
if (uploadedUrl) {
const node = schema.nodes.image.create({ src: uploadedUrl, alt: file.name });
nodes.push(node);
}
} catch (err) {
console.error("[Milkdown] Failed to upload image in custom uploader:", err);
}
}
}
return nodes;
}
}));
} catch (err) {
console.error("[Milkdown] Failed to configure uploadConfig uploader:", err);
}
});
// Store the editor instance in the map
editorCache.set(elementId, crepe);