mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +01:00 
			
		
		
		
	html sanitize imported notes, #1137
This commit is contained in:
		
							
								
								
									
										55
									
								
								db/migrations/0160__attr_def_short.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								db/migrations/0160__attr_def_short.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| const sql = require('../../src/services/sql'); | ||||
|  | ||||
| module.exports = () => { | ||||
|     for (const attr of sql.getRows("SELECT * FROM attributes WHERE name LIKE 'label:%'")) { | ||||
|         const obj = JSON.parse(attr.value); | ||||
|  | ||||
|         const tokens = []; | ||||
|  | ||||
|         if (obj.isPromoted) { | ||||
|             tokens.push('promoted'); | ||||
|         } | ||||
|  | ||||
|         if (obj.labelType) { | ||||
|             tokens.push(obj.labelType); | ||||
|         } | ||||
|  | ||||
|         if (obj.multiplicityType === 'singlevalue') { | ||||
|             tokens.push('single'); | ||||
|         } else if (obj.multiplicityType === 'multivalue') { | ||||
|             tokens.push('multi'); | ||||
|         } | ||||
|  | ||||
|         if (obj.numberPrecision) { | ||||
|             tokens.push('precision='+obj.numberPrecision); | ||||
|         } | ||||
|  | ||||
|         const newValue = tokens.join(','); | ||||
|  | ||||
|         sql.execute('UPDATE attributes SET value = ? WHERE attributeId = ?', [newValue, attr.attributeId]); | ||||
|     } | ||||
|  | ||||
|     for (const attr of sql.getRows("SELECT * FROM attributes WHERE name LIKE 'relation:%'")) { | ||||
|         const obj = JSON.parse(attr.value); | ||||
|  | ||||
|         const tokens = []; | ||||
|  | ||||
|         if (obj.isPromoted) { | ||||
|             tokens.push('promoted'); | ||||
|         } | ||||
|  | ||||
|         if (obj.inverseRelation) { | ||||
|             tokens.push('inverse=' + obj.inverseRelation); | ||||
|         } | ||||
|  | ||||
|         if (obj.multiplicityType === 'singlevalue') { | ||||
|             tokens.push('single'); | ||||
|         } else if (obj.multiplicityType === 'multivalue') { | ||||
|             tokens.push('multi'); | ||||
|         } | ||||
|  | ||||
|         const newValue = tokens.join(','); | ||||
|  | ||||
|         sql.execute('UPDATE attributes SET value = ? WHERE attributeId = ?', [newValue, attr.attributeId]); | ||||
|     } | ||||
| }; | ||||
							
								
								
									
										4432
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4432
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -50,7 +50,7 @@ | ||||
|     "image-type": "4.1.0", | ||||
|     "ini": "1.3.5", | ||||
|     "is-svg": "4.2.1", | ||||
|     "jimp": "0.13.0", | ||||
|     "jimp": "0.14.0", | ||||
|     "mime-types": "2.1.27", | ||||
|     "multer": "1.4.2", | ||||
|     "node-abi": "2.18.0", | ||||
| @@ -60,6 +60,7 @@ | ||||
|     "rcedit": "2.2.0", | ||||
|     "rimraf": "3.0.2", | ||||
|     "sanitize-filename": "1.6.3", | ||||
|     "sanitize-html": "^1.27.0", | ||||
|     "sax": "1.2.4", | ||||
|     "semver": "7.3.2", | ||||
|     "serve-favicon": "2.5.0", | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import treeService from "../../services/tree.js"; | ||||
| const TPL = ` | ||||
| <div class="note-detail-readonly-text note-detail-printable"> | ||||
|     <style> | ||||
|     /* h1 should not be used at all since semantically that's a note title */ | ||||
|     .note-detail-readonly-text h1 { font-size: 2.0em; } | ||||
|     .note-detail-readonly-text h2 { font-size: 1.8em; } | ||||
|     .note-detail-readonly-text h3 { font-size: 1.6em; } | ||||
|   | ||||
| @@ -4,7 +4,7 @@ const build = require('./build'); | ||||
| const packageJson = require('../../package'); | ||||
| const {TRILIUM_DATA_DIR} = require('./data_dir'); | ||||
|  | ||||
| const APP_DB_VERSION = 159; | ||||
| const APP_DB_VERSION = 160; | ||||
| const SYNC_VERSION = 14; | ||||
| const CLIPPER_PROTOCOL_VERSION = "1.0"; | ||||
|  | ||||
|   | ||||
							
								
								
									
										29
									
								
								src/services/html_sanitizer.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/services/html_sanitizer.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| const sanitizeHtml = require('sanitize-html'); | ||||
|  | ||||
| // intended mainly as protection against XSS via import | ||||
| // secondarily it (partly) protects against "CSS takeover" | ||||
| function sanitize(dirtyHtml) { | ||||
|     return sanitizeHtml(dirtyHtml, { | ||||
|         allowedTags: [ | ||||
|             // h1 is removed since that should be note's title | ||||
|             'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', | ||||
|             'li', 'b', 'i', 'strong', 'em', 'strike', 'abbr', 'code', 'hr', 'br', 'div', | ||||
|             'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'section', 'figure', 'span', | ||||
|             'label', 'input' | ||||
|         ], | ||||
|         allowedAttributes: { | ||||
|             'a': [ 'href', 'class' ], | ||||
|             'img': [ 'src' ], | ||||
|             'section': [ 'class', 'data-note-id' ], | ||||
|             'figure': [ 'class' ], | ||||
|             'span': [ 'class', 'style' ], | ||||
|             'label': [ 'class' ], | ||||
|             'input': [ 'class', 'type', 'disabled' ], | ||||
|             'code': [ 'class' ] | ||||
|         } | ||||
|     }); | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     sanitize | ||||
| }; | ||||
| @@ -7,6 +7,7 @@ const sql = require("../sql"); | ||||
| const noteService = require("../notes"); | ||||
| const imageService = require("../image"); | ||||
| const protectedSessionService = require('../protected_session'); | ||||
| const htmlSanitizer = require("../html_sanitizer"); | ||||
|  | ||||
| // date format is e.g. 20181121T193703Z | ||||
| function parseDate(text) { | ||||
| @@ -71,6 +72,8 @@ function importEnex(taskContext, file, parentNote) { | ||||
|         content = content.replace(/<\/ol>\s+<\/ol>/g, "</ol></li></ol>"); | ||||
|         content = content.replace(/<\/ol>\s+<li>/g, "</ol></li><li>"); | ||||
|  | ||||
|         content = htmlSanitizer.sanitize(content); | ||||
|  | ||||
|         return content; | ||||
|     } | ||||
|  | ||||
| @@ -295,6 +298,8 @@ function importEnex(taskContext, file, parentNote) { | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         content = htmlSanitizer.sanitize(content); | ||||
|  | ||||
|         // save updated content with links to files/images | ||||
|         noteEntity.setContent(content); | ||||
|  | ||||
|   | ||||
| @@ -3,6 +3,7 @@ | ||||
| const noteService = require('../../services/notes'); | ||||
| const parseString = require('xml2js').parseString; | ||||
| const protectedSessionService = require('../protected_session'); | ||||
| const htmlSanitizer = require('../html_sanitizer'); | ||||
|  | ||||
| /** | ||||
|  * @param {TaskContext} taskContext | ||||
| @@ -44,6 +45,8 @@ function importOpml(taskContext, fileBuffer, parentNote) { | ||||
|             throw new Error("Unrecognized OPML version " + opmlVersion); | ||||
|         } | ||||
|  | ||||
|         content = htmlSanitizer.sanitize(content); | ||||
|  | ||||
|         const {note} = noteService.createNewNote({ | ||||
|             parentNoteId, | ||||
|             title, | ||||
|   | ||||
| @@ -6,6 +6,7 @@ const protectedSessionService = require('../protected_session'); | ||||
| const commonmark = require('commonmark'); | ||||
| const mimeService = require('./mime'); | ||||
| const utils = require('../../services/utils'); | ||||
| const htmlSanitizer = require('../html_sanitizer'); | ||||
|  | ||||
| function importSingleFile(taskContext, file, parentNote) { | ||||
|     const mime = mimeService.getMime(file.originalname) || file.mimetype; | ||||
| @@ -122,7 +123,9 @@ function importMarkdown(taskContext, file, parentNote) { | ||||
|     const writer = new commonmark.HtmlRenderer(); | ||||
|  | ||||
|     const parsed = reader.parse(markdownContent); | ||||
|     const htmlContent = writer.render(parsed); | ||||
|     let htmlContent = writer.render(parsed); | ||||
|  | ||||
|     htmlContent = htmlSanitizer.sanitize(htmlContent); | ||||
|  | ||||
|     const title = utils.getNoteTitle(file.originalname, taskContext.data.replaceUnderscoresWithSpaces); | ||||
|  | ||||
| @@ -142,7 +145,9 @@ function importMarkdown(taskContext, file, parentNote) { | ||||
|  | ||||
| function importHtml(taskContext, file, parentNote) { | ||||
|     const title = utils.getNoteTitle(file.originalname, taskContext.data.replaceUnderscoresWithSpaces); | ||||
|     const content = file.buffer.toString("UTF-8"); | ||||
|     let content = file.buffer.toString("UTF-8"); | ||||
|  | ||||
|     content = htmlSanitizer.sanitize(content); | ||||
|  | ||||
|     const {note} = noteService.createNewNote({ | ||||
|         parentNoteId: parentNote.noteId, | ||||
|   | ||||
| @@ -16,6 +16,7 @@ const protectedSessionService = require('../protected_session'); | ||||
| const mimeService = require("./mime"); | ||||
| const sql = require("../sql"); | ||||
| const treeService = require("../tree"); | ||||
| const htmlSanitizer = require("../html_sanitizer"); | ||||
|  | ||||
| /** | ||||
|  * @param {TaskContext} taskContext | ||||
| @@ -255,6 +256,8 @@ async function importTar(taskContext, fileBuffer, importRootNote) { | ||||
|                 return /^(?:[a-z]+:)?\/\//i.test(url); | ||||
|             } | ||||
|  | ||||
|             content = htmlSanitizer.sanitize(content); | ||||
|  | ||||
|             content = content.replace(/<html.*<body[^>]*>/gis, ""); | ||||
|             content = content.replace(/<\/body>.*<\/html>/gis, ""); | ||||
|  | ||||
|   | ||||
| @@ -14,6 +14,7 @@ const protectedSessionService = require('../protected_session'); | ||||
| const mimeService = require("./mime"); | ||||
| const treeService = require("../tree"); | ||||
| const yauzl = require("yauzl"); | ||||
| const htmlSanitizer = require('../html_sanitizer'); | ||||
|  | ||||
| /** | ||||
|  * @param {TaskContext} taskContext | ||||
| @@ -269,6 +270,17 @@ async function importZip(taskContext, fileBuffer, importRootNote) { | ||||
|                 return /^(?:[a-z]+:)?\/\//i.test(url); | ||||
|             } | ||||
|  | ||||
|             content = content.replace(/<h1>([^<]*)<\/h1>/gi, (match, text) => { | ||||
|                 if (noteTitle.trim() === text.trim()) { | ||||
|                     return ""; // remove whole H1 tag | ||||
|                 } | ||||
|                 else { | ||||
|                     return match; | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             content = htmlSanitizer.sanitize(content); | ||||
|  | ||||
|             content = content.replace(/<html.*<body[^>]*>/gis, ""); | ||||
|             content = content.replace(/<\/body>.*<\/html>/gis, ""); | ||||
|  | ||||
| @@ -296,15 +308,6 @@ async function importZip(taskContext, fileBuffer, importRootNote) { | ||||
|                 return `href="#root/${targetNoteId}"`; | ||||
|             }); | ||||
|  | ||||
|             content = content.replace(/<h1>([^<]*)<\/h1>/gi, (match, text) => { | ||||
|                 if (noteTitle.trim() === text.trim()) { | ||||
|                     return ""; // remove whole H1 tag | ||||
|                 } | ||||
|                 else { | ||||
|                     return match; | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             if (noteMeta) { | ||||
|                 const includeNoteLinks = (noteMeta.attributes || []) | ||||
|                     .filter(attr => attr.type === 'relation' && attr.name === 'includeNoteLink'); | ||||
|   | ||||
| @@ -12,6 +12,11 @@ class Attribute { | ||||
|         this.type = row.type; | ||||
|         /** @param {string} */ | ||||
|         this.name = row.name.toLowerCase(); | ||||
|  | ||||
|         if (typeof row.value !== 'string') { | ||||
|             row.value = JSON.stringify(row.value); | ||||
|         } | ||||
|  | ||||
|         /** @param {string} */ | ||||
|         this.value = row.type === 'label' ? row.value.toLowerCase() : row.value; | ||||
|         /** @param {boolean} */ | ||||
|   | ||||
		Reference in New Issue
	
	Block a user