diff --git a/resources/js/wysiwyg/index.ts b/resources/js/wysiwyg/index.ts
index d7f873ea5..fdcfa5b7e 100644
--- a/resources/js/wysiwyg/index.ts
+++ b/resources/js/wysiwyg/index.ts
@@ -8,7 +8,7 @@ import {getEditorContentAsHtml, setEditorContentFromHtml} from "./utils/actions"
 import {registerTableResizer} from "./ui/framework/helpers/table-resizer";
 import {EditorUiContext} from "./ui/framework/core";
 import {listen as listenToCommonEvents} from "./services/common-events";
-import {handleDropEvents} from "./services/drop-handling";
+import {registerDropPasteHandling} from "./services/drop-paste-handling";
 import {registerTaskListHandler} from "./ui/framework/helpers/task-list-handler";
 import {registerTableSelectionHandler} from "./ui/framework/helpers/table-selection-handler";
 import {el} from "./utils/dom";
@@ -54,10 +54,10 @@ export function createPageEditorInstance(container: HTMLElement, htmlContent: st
         registerTableResizer(editor, editWrap),
         registerTableSelectionHandler(editor),
         registerTaskListHandler(editor, editArea),
+        registerDropPasteHandling(context),
     );
 
     listenToCommonEvents(editor);
-    handleDropEvents(editor);
 
     setEditorContentFromHtml(editor, htmlContent);
 
diff --git a/resources/js/wysiwyg/services/drop-handling.ts b/resources/js/wysiwyg/services/drop-handling.ts
deleted file mode 100644
index 7c9bb2713..000000000
--- a/resources/js/wysiwyg/services/drop-handling.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import {
-    $isDecoratorNode,
-    LexicalEditor,
-    LexicalNode
-} from "lexical";
-import {$insertNewBlockNodesAtSelection, $selectSingleNode} from "../utils/selection";
-import {$getNearestBlockNodeForCoords, $htmlToBlockNodes} from "../utils/nodes";
-
-function $getNodeFromMouseEvent(event: MouseEvent, editor: LexicalEditor): LexicalNode|null {
-    const x = event.clientX;
-    const y = event.clientY;
-    const dom = document.elementFromPoint(x, y);
-    if (!dom) {
-        return null;
-    }
-
-    return $getNearestBlockNodeForCoords(editor, event.clientX, event.clientY);
-}
-
-function $insertNodesAtEvent(nodes: LexicalNode[], event: DragEvent, editor: LexicalEditor) {
-    const positionNode = $getNodeFromMouseEvent(event, editor);
-
-    if (positionNode) {
-        $selectSingleNode(positionNode);
-    }
-
-    $insertNewBlockNodesAtSelection(nodes, true);
-
-    if (!$isDecoratorNode(positionNode) || !positionNode?.getTextContent()) {
-        positionNode?.remove();
-    }
-}
-
-async function insertTemplateToEditor(editor: LexicalEditor, templateId: string, event: DragEvent) {
-    const resp = await window.$http.get(`/templates/${templateId}`);
-    const data = (resp.data || {html: ''}) as {html: string}
-    const html: string = data.html || '';
-
-    editor.update(() => {
-        const newNodes = $htmlToBlockNodes(editor, html);
-        $insertNodesAtEvent(newNodes, event, editor);
-    });
-}
-
-function createDropListener(editor: LexicalEditor): (event: DragEvent) => void {
-    return (event: DragEvent) => {
-        // Template handling
-        const templateId = event.dataTransfer?.getData('bookstack/template') || '';
-        if (templateId) {
-            insertTemplateToEditor(editor, templateId, event);
-            event.preventDefault();
-            return;
-        }
-
-        // HTML contents drop
-        const html = event.dataTransfer?.getData('text/html') || '';
-        if (html) {
-            editor.update(() => {
-                const newNodes = $htmlToBlockNodes(editor, html);
-                $insertNodesAtEvent(newNodes, event, editor);
-            });
-            event.preventDefault();
-            return;
-        }
-    };
-}
-
-export function handleDropEvents(editor: LexicalEditor) {
-    const dropListener = createDropListener(editor);
-
-    editor.registerRootListener((rootElement, prevRootElement) => {
-        rootElement?.addEventListener('drop', dropListener);
-        prevRootElement?.removeEventListener('drop', dropListener);
-    });
-}
\ No newline at end of file
diff --git a/resources/js/wysiwyg/services/drop-paste-handling.ts b/resources/js/wysiwyg/services/drop-paste-handling.ts
new file mode 100644
index 000000000..85d0235d8
--- /dev/null
+++ b/resources/js/wysiwyg/services/drop-paste-handling.ts
@@ -0,0 +1,158 @@
+import {
+    $insertNodes,
+    $isDecoratorNode, COMMAND_PRIORITY_HIGH, DROP_COMMAND,
+    LexicalEditor,
+    LexicalNode, PASTE_COMMAND
+} from "lexical";
+import {$insertNewBlockNodesAtSelection, $selectSingleNode} from "../utils/selection";
+import {$getNearestBlockNodeForCoords, $htmlToBlockNodes} from "../utils/nodes";
+import {Clipboard} from "../../services/clipboard";
+import {$createImageNode} from "../nodes/image";
+import {$createCustomParagraphNode} from "../nodes/custom-paragraph";
+import {$createLinkNode} from "@lexical/link";
+import {EditorImageData, uploadImageFile} from "../utils/images";
+import {EditorUiContext} from "../ui/framework/core";
+
+function $getNodeFromMouseEvent(event: MouseEvent, editor: LexicalEditor): LexicalNode|null {
+    const x = event.clientX;
+    const y = event.clientY;
+    const dom = document.elementFromPoint(x, y);
+    if (!dom) {
+        return null;
+    }
+
+    return $getNearestBlockNodeForCoords(editor, event.clientX, event.clientY);
+}
+
+function $insertNodesAtEvent(nodes: LexicalNode[], event: DragEvent, editor: LexicalEditor) {
+    const positionNode = $getNodeFromMouseEvent(event, editor);
+
+    if (positionNode) {
+        $selectSingleNode(positionNode);
+    }
+
+    $insertNewBlockNodesAtSelection(nodes, true);
+
+    if (!$isDecoratorNode(positionNode) || !positionNode?.getTextContent()) {
+        positionNode?.remove();
+    }
+}
+
+async function insertTemplateToEditor(editor: LexicalEditor, templateId: string, event: DragEvent) {
+    const resp = await window.$http.get(`/templates/${templateId}`);
+    const data = (resp.data || {html: ''}) as {html: string}
+    const html: string = data.html || '';
+
+    editor.update(() => {
+        const newNodes = $htmlToBlockNodes(editor, html);
+        $insertNodesAtEvent(newNodes, event, editor);
+    });
+}
+
+function handleMediaInsert(data: DataTransfer, context: EditorUiContext): boolean {
+    const clipboard = new Clipboard(data);
+    let handled = false;
+
+    // Don't handle the event ourselves if no items exist of contains table-looking data
+    if (!clipboard.hasItems() || clipboard.containsTabularData()) {
+        return handled;
+    }
+
+    const images = clipboard.getImages();
+    if (images.length > 0) {
+        handled = true;
+    }
+
+    context.editor.update(async () => {
+        for (const imageFile of images) {
+            const loadingImage = window.baseUrl('/loading.gif');
+            const loadingNode = $createImageNode(loadingImage);
+            const imageWrap = $createCustomParagraphNode();
+            imageWrap.append(loadingNode);
+            $insertNodes([imageWrap]);
+
+            try {
+                const respData: EditorImageData = await uploadImageFile(imageFile, context.options.pageId);
+                const safeName = respData.name.replace(/"/g, '');
+                context.editor.update(() => {
+                    const finalImage = $createImageNode(respData.thumbs?.display || '', {
+                        alt: safeName,
+                    });
+                    const imageLink = $createLinkNode(respData.url, {target: '_blank'});
+                    imageLink.append(finalImage);
+                    loadingNode.replace(imageLink);
+                });
+            } catch (err: any) {
+                context.editor.update(() => {
+                    loadingNode.remove(false);
+                });
+                window.$events.error(err?.data?.message || context.options.translations.imageUploadErrorText);
+                console.error(err);
+            }
+        }
+    });
+
+    return handled;
+}
+
+function createDropListener(context: EditorUiContext): (event: DragEvent) => boolean {
+    const editor = context.editor;
+    return (event: DragEvent): boolean => {
+        // Template handling
+        const templateId = event.dataTransfer?.getData('bookstack/template') || '';
+        if (templateId) {
+            insertTemplateToEditor(editor, templateId, event);
+            event.preventDefault();
+            return true;
+        }
+
+        // HTML contents drop
+        const html = event.dataTransfer?.getData('text/html') || '';
+        if (html) {
+            editor.update(() => {
+                const newNodes = $htmlToBlockNodes(editor, html);
+                $insertNodesAtEvent(newNodes, event, editor);
+            });
+            event.preventDefault();
+            return true;
+        }
+
+        if (event.dataTransfer) {
+            const handled = handleMediaInsert(event.dataTransfer, context);
+            if (handled) {
+                event.preventDefault();
+                return true;
+            }
+        }
+
+        return false;
+    };
+}
+
+function createPasteListener(context: EditorUiContext): (event: ClipboardEvent) => boolean {
+    return (event: ClipboardEvent) => {
+        if (!event.clipboardData) {
+            return false;
+        }
+
+        const handled = handleMediaInsert(event.clipboardData, context);
+        if (handled) {
+            event.preventDefault();
+        }
+
+        return handled;
+    };
+}
+
+export function registerDropPasteHandling(context: EditorUiContext): () => void {
+    const dropListener = createDropListener(context);
+    const pasteListener = createPasteListener(context);
+
+    const unregisterDrop = context.editor.registerCommand(DROP_COMMAND, dropListener, COMMAND_PRIORITY_HIGH);
+    const unregisterPaste = context.editor.registerCommand(PASTE_COMMAND, pasteListener, COMMAND_PRIORITY_HIGH);
+
+    return () => {
+        unregisterDrop();
+        unregisterPaste();
+    };
+}
\ No newline at end of file
diff --git a/resources/js/wysiwyg/todo.md b/resources/js/wysiwyg/todo.md
index f05e79baa..f339a6ed4 100644
--- a/resources/js/wysiwyg/todo.md
+++ b/resources/js/wysiwyg/todo.md
@@ -6,9 +6,7 @@
 
 ## Main Todo
 
-
 - Alignments: Handle inline block content (image, video)
-- Image paste upload
 - Support media src conversions (https://github.com/tinymce/tinymce/blob/release/6.6/modules/tinymce/src/plugins/media/main/ts/core/UrlPatterns.ts)
 - Media resize support (like images)
 - Table caption text support
diff --git a/resources/js/wysiwyg/utils/images.ts b/resources/js/wysiwyg/utils/images.ts
index a83d55418..2c13427d9 100644
--- a/resources/js/wysiwyg/utils/images.ts
+++ b/resources/js/wysiwyg/utils/images.ts
@@ -24,4 +24,21 @@ export function $createLinkedImageNodeFromImageData(image: EditorImageData): Lin
     });
     linkNode.append(imageNode);
     return linkNode;
+}
+
+/**
+ * Upload an image file to the server
+ */
+export async function uploadImageFile(file: File, pageId: string): Promise<EditorImageData> {
+    if (file === null || file.type.indexOf('image') !== 0) {
+        throw new Error('Not an image file');
+    }
+
+    const remoteFilename = file.name || `image-${Date.now()}.png`;
+    const formData = new FormData();
+    formData.append('file', file, remoteFilename);
+    formData.append('uploaded_to', pageId);
+
+    const resp = await window.$http.post('/images/gallery', formData);
+    return resp.data as EditorImageData;
 }
\ No newline at end of file