Merge pull request #2344 from raizasafeel/security

fix: sanitize lesson blocks from server and client
This commit is contained in:
Raizaaa
2026-05-01 21:58:46 +05:30
committed by GitHub
6 changed files with 68 additions and 7 deletions
+4 -1
View File
@@ -57,7 +57,7 @@
>
</iframe>
</div>
<div v-else v-html="markdown.render(block)"></div>
<div v-else v-html="renderSafe(block)"></div>
</div>
<div v-if="quizId">
<Quiz :quiz="quizId" />
@@ -66,6 +66,7 @@
<script setup>
import Quiz from '@/components/QuizBlock.vue'
import MarkdownIt from 'markdown-it'
import DOMPurify from 'dompurify'
import { useScreenSize } from '@/utils/composables'
const screenSize = useScreenSize()
@@ -75,6 +76,8 @@ const markdown = new MarkdownIt({
linkify: true,
})
const renderSafe = (block) => DOMPurify.sanitize(markdown.render(block))
const props = defineProps({
content: {
type: String,
+7 -2
View File
@@ -365,7 +365,12 @@ import {
MessageCircleQuestion,
TrendingUp,
} from 'lucide-vue-next'
import { getEditorTools, enablePlyr, highlightText } from '@/utils'
import {
getEditorTools,
enablePlyr,
highlightText,
sanitizeEditorJs,
} from '@/utils'
import { sessionStore } from '@/stores/session'
import { useSidebar } from '@/stores/sidebar'
import EditorJS from '@editorjs/editorjs'
@@ -511,7 +516,7 @@ const renderEditor = (holder, content) => {
return new EditorJS({
holder: holder,
tools: getEditorTools(),
data: JSON.parse(content),
data: sanitizeEditorJs(JSON.parse(content)),
readOnly: true,
defaultBlock: 'embed',
i18n: {
+5 -3
View File
@@ -104,7 +104,7 @@ import { sessionStore } from '../stores/session'
import EditorJS from '@editorjs/editorjs'
import LessonHelp from '@/components/LessonHelp.vue'
import { ChevronRight } from 'lucide-vue-next'
import { getEditorTools, enablePlyr } from '@/utils'
import { getEditorTools, enablePlyr, sanitizeEditorJs } from '@/utils'
import { useOnboarding, useTelemetry } from 'frappe-ui/frappe'
const { brand } = sessionStore()
@@ -191,7 +191,7 @@ const lessonDetails = createResource({
const addLessonContent = (data) => {
editor.value.isReady.then(() => {
if (data.lesson.content) {
editor.value.render(JSON.parse(data.lesson.content))
editor.value.render(sanitizeEditorJs(JSON.parse(data.lesson.content)))
} else if (data.lesson.body) {
let blocks = convertToJSON(data.lesson)
editor.value.render({
@@ -204,7 +204,9 @@ const addLessonContent = (data) => {
const addInstructorNotes = (data) => {
instructorEditor.value.isReady.then(() => {
if (data.lesson.instructor_content) {
instructorEditor.value.render(JSON.parse(data.lesson.instructor_content))
instructorEditor.value.render(
sanitizeEditorJs(JSON.parse(data.lesson.instructor_content))
)
} else if (data.lesson.instructor_notes) {
let blocks = convertToJSON(data.lesson)
instructorEditor.value.render({
+28
View File
@@ -706,6 +706,34 @@ export const escapeHTML = (text) => {
)
}
const sanitizeJSON = (node) => {
if (Array.isArray(node)) return node.map(sanitizeJSON)
if (node && typeof node === 'object') {
const temp = {}
for (const n in node) {
temp[n] = sanitizeJSON(node[n])
}
return temp
}
if (
typeof node === 'string' &&
(node.includes('<') || node.includes('>'))
) {
return DOMPurify.sanitize(node)
}
return node
}
export const sanitizeEditorJs = (data) => {
if (!data || !Array.isArray(data.blocks)) return data
for (const node of data.blocks) {
if (node && node.type !== 'code') {
node.data = sanitizeJSON(node.data)
}
}
return data
}
export const sanitizeHTML = (text) => {
text = DOMPurify.sanitize(decodeEntities(text), {
ALLOWED_TAGS: [
@@ -9,7 +9,7 @@ from frappe.model.document import Document
from frappe.realtime import get_website_room
from frappe.utils.telemetry import capture
from lms.lms.utils import get_course_progress, is_demo_course, recalculate_course_progress
from lms.lms.utils import get_course_progress, is_demo_course, recalculate_course_progress, sanitize_editorjs
from ...md import find_macros
@@ -21,6 +21,10 @@ class CourseLesson(Document):
def after_delete(self):
self.validate_progress_recalculation()
def validate(self):
self.content = sanitize_editorjs(self.content)
self.instructor_content = sanitize_editorjs(self.instructor_content)
def on_update(self):
self.validate_quiz_id()
+19
View File
@@ -25,6 +25,7 @@ from frappe.utils import (
rounded,
validate_email_address,
)
from frappe.utils.html_utils import sanitize_html
from pypika import Case
from pypika import functions as fn
@@ -2398,3 +2399,21 @@ def get_field_meta(doctype, fieldnames):
def is_demo_course(course: str) -> bool:
title = frappe.db.get_value("LMS Course", course, "title")
return title == "A guide to Frappe Learning"
def sanitize_editorjs(raw):
try:
data = json.loads(raw)
except (TypeError, ValueError):
return raw
return json.dumps(sanitize_json(data), separators=(",", ":"))
def sanitize_json(node):
if isinstance(node, dict):
return {k: sanitize_json(v) for k, v in node.items()}
if isinstance(node, list):
return [sanitize_json(v) for v in node]
if isinstance(node, str) and ("<" in node or ">" in node):
return sanitize_html(node, always_sanitize=True)
return node