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", |     "image-type": "4.1.0", | ||||||
|     "ini": "1.3.5", |     "ini": "1.3.5", | ||||||
|     "is-svg": "4.2.1", |     "is-svg": "4.2.1", | ||||||
|     "jimp": "0.13.0", |     "jimp": "0.14.0", | ||||||
|     "mime-types": "2.1.27", |     "mime-types": "2.1.27", | ||||||
|     "multer": "1.4.2", |     "multer": "1.4.2", | ||||||
|     "node-abi": "2.18.0", |     "node-abi": "2.18.0", | ||||||
| @@ -60,6 +60,7 @@ | |||||||
|     "rcedit": "2.2.0", |     "rcedit": "2.2.0", | ||||||
|     "rimraf": "3.0.2", |     "rimraf": "3.0.2", | ||||||
|     "sanitize-filename": "1.6.3", |     "sanitize-filename": "1.6.3", | ||||||
|  |     "sanitize-html": "^1.27.0", | ||||||
|     "sax": "1.2.4", |     "sax": "1.2.4", | ||||||
|     "semver": "7.3.2", |     "semver": "7.3.2", | ||||||
|     "serve-favicon": "2.5.0", |     "serve-favicon": "2.5.0", | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ import treeService from "../../services/tree.js"; | |||||||
| const TPL = ` | const TPL = ` | ||||||
| <div class="note-detail-readonly-text note-detail-printable"> | <div class="note-detail-readonly-text note-detail-printable"> | ||||||
|     <style> |     <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 h1 { font-size: 2.0em; } | ||||||
|     .note-detail-readonly-text h2 { font-size: 1.8em; } |     .note-detail-readonly-text h2 { font-size: 1.8em; } | ||||||
|     .note-detail-readonly-text h3 { font-size: 1.6em; } |     .note-detail-readonly-text h3 { font-size: 1.6em; } | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ const build = require('./build'); | |||||||
| const packageJson = require('../../package'); | const packageJson = require('../../package'); | ||||||
| const {TRILIUM_DATA_DIR} = require('./data_dir'); | const {TRILIUM_DATA_DIR} = require('./data_dir'); | ||||||
|  |  | ||||||
| const APP_DB_VERSION = 159; | const APP_DB_VERSION = 160; | ||||||
| const SYNC_VERSION = 14; | const SYNC_VERSION = 14; | ||||||
| const CLIPPER_PROTOCOL_VERSION = "1.0"; | 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 noteService = require("../notes"); | ||||||
| const imageService = require("../image"); | const imageService = require("../image"); | ||||||
| const protectedSessionService = require('../protected_session'); | const protectedSessionService = require('../protected_session'); | ||||||
|  | const htmlSanitizer = require("../html_sanitizer"); | ||||||
|  |  | ||||||
| // date format is e.g. 20181121T193703Z | // date format is e.g. 20181121T193703Z | ||||||
| function parseDate(text) { | 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+<\/ol>/g, "</ol></li></ol>"); | ||||||
|         content = content.replace(/<\/ol>\s+<li>/g, "</ol></li><li>"); |         content = content.replace(/<\/ol>\s+<li>/g, "</ol></li><li>"); | ||||||
|  |  | ||||||
|  |         content = htmlSanitizer.sanitize(content); | ||||||
|  |  | ||||||
|         return content; |         return content; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -295,6 +298,8 @@ function importEnex(taskContext, file, parentNote) { | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         content = htmlSanitizer.sanitize(content); | ||||||
|  |  | ||||||
|         // save updated content with links to files/images |         // save updated content with links to files/images | ||||||
|         noteEntity.setContent(content); |         noteEntity.setContent(content); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ | |||||||
| const noteService = require('../../services/notes'); | const noteService = require('../../services/notes'); | ||||||
| const parseString = require('xml2js').parseString; | const parseString = require('xml2js').parseString; | ||||||
| const protectedSessionService = require('../protected_session'); | const protectedSessionService = require('../protected_session'); | ||||||
|  | const htmlSanitizer = require('../html_sanitizer'); | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * @param {TaskContext} taskContext |  * @param {TaskContext} taskContext | ||||||
| @@ -44,6 +45,8 @@ function importOpml(taskContext, fileBuffer, parentNote) { | |||||||
|             throw new Error("Unrecognized OPML version " + opmlVersion); |             throw new Error("Unrecognized OPML version " + opmlVersion); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         content = htmlSanitizer.sanitize(content); | ||||||
|  |  | ||||||
|         const {note} = noteService.createNewNote({ |         const {note} = noteService.createNewNote({ | ||||||
|             parentNoteId, |             parentNoteId, | ||||||
|             title, |             title, | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ const protectedSessionService = require('../protected_session'); | |||||||
| const commonmark = require('commonmark'); | const commonmark = require('commonmark'); | ||||||
| const mimeService = require('./mime'); | const mimeService = require('./mime'); | ||||||
| const utils = require('../../services/utils'); | const utils = require('../../services/utils'); | ||||||
|  | const htmlSanitizer = require('../html_sanitizer'); | ||||||
|  |  | ||||||
| function importSingleFile(taskContext, file, parentNote) { | function importSingleFile(taskContext, file, parentNote) { | ||||||
|     const mime = mimeService.getMime(file.originalname) || file.mimetype; |     const mime = mimeService.getMime(file.originalname) || file.mimetype; | ||||||
| @@ -122,7 +123,9 @@ function importMarkdown(taskContext, file, parentNote) { | |||||||
|     const writer = new commonmark.HtmlRenderer(); |     const writer = new commonmark.HtmlRenderer(); | ||||||
|  |  | ||||||
|     const parsed = reader.parse(markdownContent); |     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); |     const title = utils.getNoteTitle(file.originalname, taskContext.data.replaceUnderscoresWithSpaces); | ||||||
|  |  | ||||||
| @@ -142,7 +145,9 @@ function importMarkdown(taskContext, file, parentNote) { | |||||||
|  |  | ||||||
| function importHtml(taskContext, file, parentNote) { | function importHtml(taskContext, file, parentNote) { | ||||||
|     const title = utils.getNoteTitle(file.originalname, taskContext.data.replaceUnderscoresWithSpaces); |     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({ |     const {note} = noteService.createNewNote({ | ||||||
|         parentNoteId: parentNote.noteId, |         parentNoteId: parentNote.noteId, | ||||||
|   | |||||||
| @@ -16,6 +16,7 @@ const protectedSessionService = require('../protected_session'); | |||||||
| const mimeService = require("./mime"); | const mimeService = require("./mime"); | ||||||
| const sql = require("../sql"); | const sql = require("../sql"); | ||||||
| const treeService = require("../tree"); | const treeService = require("../tree"); | ||||||
|  | const htmlSanitizer = require("../html_sanitizer"); | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * @param {TaskContext} taskContext |  * @param {TaskContext} taskContext | ||||||
| @@ -255,6 +256,8 @@ async function importTar(taskContext, fileBuffer, importRootNote) { | |||||||
|                 return /^(?:[a-z]+:)?\/\//i.test(url); |                 return /^(?:[a-z]+:)?\/\//i.test(url); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |             content = htmlSanitizer.sanitize(content); | ||||||
|  |  | ||||||
|             content = content.replace(/<html.*<body[^>]*>/gis, ""); |             content = content.replace(/<html.*<body[^>]*>/gis, ""); | ||||||
|             content = content.replace(/<\/body>.*<\/html>/gis, ""); |             content = content.replace(/<\/body>.*<\/html>/gis, ""); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -14,6 +14,7 @@ const protectedSessionService = require('../protected_session'); | |||||||
| const mimeService = require("./mime"); | const mimeService = require("./mime"); | ||||||
| const treeService = require("../tree"); | const treeService = require("../tree"); | ||||||
| const yauzl = require("yauzl"); | const yauzl = require("yauzl"); | ||||||
|  | const htmlSanitizer = require('../html_sanitizer'); | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * @param {TaskContext} taskContext |  * @param {TaskContext} taskContext | ||||||
| @@ -269,6 +270,17 @@ async function importZip(taskContext, fileBuffer, importRootNote) { | |||||||
|                 return /^(?:[a-z]+:)?\/\//i.test(url); |                 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(/<html.*<body[^>]*>/gis, ""); | ||||||
|             content = content.replace(/<\/body>.*<\/html>/gis, ""); |             content = content.replace(/<\/body>.*<\/html>/gis, ""); | ||||||
|  |  | ||||||
| @@ -296,15 +308,6 @@ async function importZip(taskContext, fileBuffer, importRootNote) { | |||||||
|                 return `href="#root/${targetNoteId}"`; |                 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) { |             if (noteMeta) { | ||||||
|                 const includeNoteLinks = (noteMeta.attributes || []) |                 const includeNoteLinks = (noteMeta.attributes || []) | ||||||
|                     .filter(attr => attr.type === 'relation' && attr.name === 'includeNoteLink'); |                     .filter(attr => attr.type === 'relation' && attr.name === 'includeNoteLink'); | ||||||
|   | |||||||
| @@ -12,6 +12,11 @@ class Attribute { | |||||||
|         this.type = row.type; |         this.type = row.type; | ||||||
|         /** @param {string} */ |         /** @param {string} */ | ||||||
|         this.name = row.name.toLowerCase(); |         this.name = row.name.toLowerCase(); | ||||||
|  |  | ||||||
|  |         if (typeof row.value !== 'string') { | ||||||
|  |             row.value = JSON.stringify(row.value); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         /** @param {string} */ |         /** @param {string} */ | ||||||
|         this.value = row.type === 'label' ? row.value.toLowerCase() : row.value; |         this.value = row.type === 'label' ? row.value.toLowerCase() : row.value; | ||||||
|         /** @param {boolean} */ |         /** @param {boolean} */ | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user