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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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