mirror of
https://github.com/zadam/trilium.git
synced 2025-03-01 14:22:32 +01:00
Merge remote-tracking branch 'origin/stable'
This commit is contained in:
commit
c2c724aa00
@ -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%")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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);
|
||||||
|
@ -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">×</span>
|
<span aria-hidden="true">×</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);
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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) {
|
||||||
|
@ -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 } );
|
||||||
|
|
||||||
|
@ -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}))
|
||||||
);
|
);
|
||||||
|
@ -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');
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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();
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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/')
|
||||||
|
Loading…
x
Reference in New Issue
Block a user