mirror of
https://github.com/frappe/lms.git
synced 2026-05-02 13:39:31 +03:00
Merge pull request #2344 from raizasafeel/security
fix: sanitize lesson blocks from server and client
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user