html sanitize imported notes, #1137

This commit is contained in:
zadam 2020-06-30 23:37:06 +02:00
parent 51f094f87f
commit 5e18e7dc67
12 changed files with 512 additions and 4056 deletions

View 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

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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; }

View File

@ -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";

View 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
};

View File

@ -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);

View File

@ -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,

View File

@ -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,

View File

@ -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, "");

View File

@ -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');

View File

@ -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} */