Merge remote-tracking branch 'origin/stable'

This commit is contained in:
zadam 2022-07-08 22:21:41 +02:00
commit c2c724aa00
14 changed files with 45 additions and 15 deletions

View File

@ -61,9 +61,11 @@ async function getRenderedContent(note, options = {}) {
$renderedContent.append($("<pre>").text(trim(fullNote.content, options.trim))); $renderedContent.append($("<pre>").text(trim(fullNote.content, options.trim)));
} }
else if (type === 'image') { else if (type === 'image') {
const sanitizedTitle = note.title.replace(/[^a-z0-9-.]/gi, "");
$renderedContent.append( $renderedContent.append(
$("<img>") $("<img>")
.attr("src", `api/images/${note.noteId}/${note.title}`) .attr("src", `api/images/${note.noteId}/${sanitizedTitle}`)
.css("max-width", "100%") .css("max-width", "100%")
); );
} }
@ -144,7 +146,7 @@ async function getRenderedContent(note, options = {}) {
else if (type === 'canvas') { else if (type === 'canvas') {
// make sure surrounding container has size of what is visible. Then image is shrinked to its boundaries // make sure surrounding container has size of what is visible. Then image is shrinked to its boundaries
$renderedContent.css({height: "100%", width:"100%"}); $renderedContent.css({height: "100%", width:"100%"});
const noteComplement = await froca.getNoteComplement(note.noteId); const noteComplement = await froca.getNoteComplement(note.noteId);
const content = noteComplement.content || ""; const content = noteComplement.content || "";

View File

@ -266,7 +266,7 @@ class NoteListRenderer {
.append($expander) .append($expander)
.append($('<span class="note-icon">').addClass(note.getIcon())) .append($('<span class="note-icon">').addClass(note.getIcon()))
.append(this.viewType === 'grid' .append(this.viewType === 'grid'
? note.title ? $("<span>").text(note.title)
: await linkService.createNoteLink(notePath, {showTooltip: false, showNotePath: this.showNotePath}) : await linkService.createNoteLink(notePath, {showTooltip: false, showNotePath: this.showNotePath})
) )
.append($renderedAttributes) .append($renderedAttributes)

View File

@ -503,7 +503,7 @@ export default class TabManager extends Component {
updateDocumentTitle(activeNoteContext) { updateDocumentTitle(activeNoteContext) {
const titleFragments = [ const titleFragments = [
// it helps navigating in history if note title is included in the title // it helps to navigate in history if note title is included in the title
activeNoteContext.note?.title, activeNoteContext.note?.title,
"Trilium Notes" "Trilium Notes"
].filter(Boolean); ].filter(Boolean);

View File

@ -4,16 +4,17 @@ import utils from "./utils.js";
function toast(options) { function toast(options) {
const $toast = $(`<div class="toast" role="alert" aria-live="assertive" aria-atomic="true"> const $toast = $(`<div class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header"> <div class="toast-header">
<strong class="mr-auto"><span class="bx bx-${options.icon}"></span> ${options.title}</strong> <strong class="mr-auto"><span class="bx bx-${options.icon}"></span> <span class="toast-title"></span></strong>
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close"> <button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<div class="toast-body"> <div class="toast-body"></div>
${options.message}
</div>
</div>`); </div>`);
$toast.find('.toast-title').text(options.title);
$toast.find('.toast-body').text(options.message);
if (options.id) { if (options.id) {
$toast.attr("id", "toast-" + options.id); $toast.attr("id", "toast-" + options.id);
} }

View File

@ -297,7 +297,7 @@ export default class ApperanceOptions {
this.$themeSelect.append($("<option>") this.$themeSelect.append($("<option>")
.attr("value", theme.val) .attr("value", theme.val)
.attr("data-note-id", theme.noteId) .attr("data-note-id", theme.noteId)
.html(theme.title)); .text(theme.title));
} }
this.$themeSelect.val(options.theme); this.$themeSelect.val(options.theme);

View File

@ -77,7 +77,9 @@ export default class EditedNotesWidget extends CollapsibleWidget {
); );
} }
else { else {
$item.append(editedNote.notePath ? await linkService.createNoteLink(editedNote.notePath.join("/"), {showNotePath: true}) : editedNote.title); $item.append(editedNote.notePath
? await linkService.createNoteLink(editedNote.notePath.join("/"), {showNotePath: true})
: $("<span>").text(editedNote.title));
} }
if (i < editedNotes.length - 1) { if (i < editedNotes.length - 1) {

View File

@ -311,7 +311,8 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
const note = await froca.getNote(noteId); const note = await froca.getNote(noteId);
this.textEditor.model.change( writer => { this.textEditor.model.change( writer => {
const src = `api/images/${note.noteId}/${note.title}`; const sanitizedTitle = note.title.replace(/[^a-z0-9-.]/gi, "");
const src = `api/images/${note.noteId}/${sanitizedTitle}`;
const imageElement = writer.createElement( 'image', { 'src': src } ); const imageElement = writer.createElement( 'image', { 'src': src } );

View File

@ -79,7 +79,7 @@ export default class EmptyTypeWidget extends TypeWidget {
this.$workspaceNotes.append( this.$workspaceNotes.append(
$('<div class="workspace-note">') $('<div class="workspace-note">')
.append($("<div>").addClass(workspaceNote.getIcon() + " workspace-icon")) .append($("<div>").addClass(workspaceNote.getIcon() + " workspace-icon"))
.append($("<div>").append(workspaceNote.title)) .append($("<div>").text(workspaceNote.title))
.attr("title", "Enter workspace " + workspaceNote.title) .attr("title", "Enter workspace " + workspaceNote.title)
.on('click', () => this.triggerCommand('hoistNote', {noteId: workspaceNote.noteId})) .on('click', () => this.triggerCommand('hoistNote', {noteId: workspaceNote.noteId}))
); );

View File

@ -43,7 +43,7 @@ function getClipperInboxNote() {
} }
function addClipping(req) { function addClipping(req) {
const {title, content, pageUrl, images} = req.body; let {title, content, pageUrl, images} = req.body;
const clipperInbox = getClipperInboxNote(); const clipperInbox = getClipperInboxNote();
@ -57,6 +57,8 @@ function addClipping(req) {
type: 'text' type: 'text'
}).note; }).note;
pageUrl = htmlSanitizer.sanitize(pageUrl);
clippingNote.setLabel('clipType', 'clippings'); clippingNote.setLabel('clipType', 'clippings');
clippingNote.setLabel('pageUrl', pageUrl); clippingNote.setLabel('pageUrl', pageUrl);
clippingNote.setLabel('iconClass', 'bx bx-globe'); clippingNote.setLabel('iconClass', 'bx bx-globe');
@ -89,9 +91,13 @@ function createNote(req) {
type: 'text' type: 'text'
}); });
clipType = htmlSanitizer.sanitize(clipType);
note.setLabel('clipType', clipType); note.setLabel('clipType', clipType);
if (pageUrl) { if (pageUrl) {
pageUrl = htmlSanitizer.sanitize(pageUrl);
note.setLabel('pageUrl', pageUrl); note.setLabel('pageUrl', pageUrl);
note.setLabel('iconClass', 'bx bx-globe'); note.setLabel('iconClass', 'bx bx-globe');
} }

View File

@ -2,6 +2,8 @@ const sanitizeHtml = require('sanitize-html');
// intended mainly as protection against XSS via import // intended mainly as protection against XSS via import
// secondarily it (partly) protects against "CSS takeover" // secondarily it (partly) protects against "CSS takeover"
// sanitize also note titles, label values etc. - there's so many usage which make it difficult to guarantee all of them
// are properly handled
function sanitize(dirtyHtml) { function sanitize(dirtyHtml) {
if (!dirtyHtml) { if (!dirtyHtml) {
return dirtyHtml; return dirtyHtml;

View File

@ -12,6 +12,7 @@ const sanitizeFilename = require('sanitize-filename');
const noteRevisionService = require('./note_revisions'); const noteRevisionService = require('./note_revisions');
const isSvg = require('is-svg'); const isSvg = require('is-svg');
const isAnimated = require('is-animated'); const isAnimated = require('is-animated');
const htmlSanitizer = require("./html_sanitizer");
async function processImage(uploadBuffer, originalName, shrinkImageSwitch) { async function processImage(uploadBuffer, originalName, shrinkImageSwitch) {
const compressImages = optionService.getOptionBool("compressImages"); const compressImages = optionService.getOptionBool("compressImages");
@ -66,6 +67,8 @@ function getImageMimeFromExtension(ext) {
function updateImage(noteId, uploadBuffer, originalName) { function updateImage(noteId, uploadBuffer, originalName) {
log.info(`Updating image ${noteId}: ${originalName}`); log.info(`Updating image ${noteId}: ${originalName}`);
originalName = htmlSanitizer.sanitize(originalName);
const note = becca.getNote(noteId); const note = becca.getNote(noteId);
note.saveNoteRevision(); note.saveNoteRevision();

View File

@ -160,6 +160,11 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
attr.name = 'disabled:' + attr.name; attr.name = 'disabled:' + attr.name;
} }
if (taskContext.data.safeImport) {
attr.name = htmlSanitizer.sanitize(attr.name);
attr.value = htmlSanitizer.sanitize(attr.value);
}
attributes.push(attr); attributes.push(attr);
} }
} }

View File

@ -18,6 +18,7 @@ const Branch = require('../becca/entities/branch');
const Note = require('../becca/entities/note'); const Note = require('../becca/entities/note');
const Attribute = require('../becca/entities/attribute'); const Attribute = require('../becca/entities/attribute');
const dayjs = require("dayjs"); const dayjs = require("dayjs");
const htmlSanitizer = require("./html_sanitizer.js");
function getNewNotePosition(parentNoteId) { function getNewNotePosition(parentNoteId) {
const note = becca.notes[parentNoteId]; const note = becca.notes[parentNoteId];
@ -98,6 +99,11 @@ function getNewNoteTitle(parentNote) {
} }
} }
// this isn't in theory a good place to sanitize title, but this will catch a lot of XSS attempts
// title is supposed to contain text only (not HTML) and be printed text only, but given the number of usages
// it's difficult to guarantee correct handling in all cases
title = htmlSanitizer.sanitize(title);
return title; return title;
} }
@ -352,8 +358,10 @@ function downloadImages(noteId, content) {
const imageService = require('../services/image'); const imageService = require('../services/image');
const {note} = imageService.saveImage(noteId, imageBuffer, "inline image", true, true); const {note} = imageService.saveImage(noteId, imageBuffer, "inline image", true, true);
const sanitizedTitle = note.title.replace(/[^a-z0-9-.]/gi, "");
content = content.substr(0, imageMatch.index) content = content.substr(0, imageMatch.index)
+ `<img src="api/images/${note.noteId}/${note.title}"` + `<img src="api/images/${note.noteId}/${sanitizedTitle}"`
+ content.substr(imageMatch.index + imageMatch[0].length); + content.substr(imageMatch.index + imageMatch[0].length);
} }
else if (!url.includes('api/images/') else if (!url.includes('api/images/')

View File

@ -241,7 +241,7 @@ function getNoteTitle(filePath, replaceUnderscoresWithSpaces, noteMeta) {
return noteMeta.title; return noteMeta.title;
} else { } else {
const basename = path.basename(removeTextFileExtension(filePath)); const basename = path.basename(removeTextFileExtension(filePath));
if(replaceUnderscoresWithSpaces) { if (replaceUnderscoresWithSpaces) {
return basename.replace(/_/g, ' ').trim(); return basename.replace(/_/g, ' ').trim();
} }
return basename; return basename;