mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-04 05:28:59 +01:00 
			
		
		
		
	html sanitize imported notes, #1137
This commit is contained in:
		
							parent
							
								
									51f094f87f
								
							
						
					
					
						commit
						5e18e7dc67
					
				
							
								
								
									
										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} */
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user