Compare commits

..

No commits in common. "bd9623578f9e31fd640c3edaa09a513382a31811" and "e08b8956943e08f96b1f40a0b663814b895bfdfe" have entirely different histories.

9 changed files with 47 additions and 78 deletions

5
package-lock.json generated
View file

@ -2412,11 +2412,6 @@
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"dev": true "dev": true
}, },
"highlight.js": {
"version": "10.3.1",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.3.1.tgz",
"integrity": "sha512-jeW8rdPdhshYKObedYg5XGbpVgb1/DT4AHvDFXhkU7UnGSIjy9kkJ7zHG7qplhFHMitTSzh5/iClKQk3Kb2RFQ=="
},
"hmac-drbg": { "hmac-drbg": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",

View file

@ -12,8 +12,7 @@
"author": "", "author": "",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"dependencies": { "dependencies": {
"dompurify": "^2.2.0", "dompurify": "^2.2.0"
"highlight.js": "^10.3.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.11.1", "@babel/core": "^7.11.1",

View file

@ -1,11 +0,0 @@
const {ElemJS} = require("../basic")
const hljs = require("highlight.js")
class HighlightedCode extends ElemJS {
constructor(code) {
super(code)
hljs.highlightBlock(this.element)
}
}
module.exports = {HighlightedCode}

View file

@ -44,9 +44,7 @@ class MatrixEvent extends ElemJS {
if (this.editedAt) { if (this.editedAt) {
this.child(ejs("span").class("c-message__edited").text("(edited)").attribute("title", "at " + dateFormatter.format(this.editedAt))) this.child(ejs("span").class("c-message__edited").text("(edited)").attribute("title", "at " + dateFormatter.format(this.editedAt)))
} }
return this
} }
static canRender(_event) { static canRender(_event) {
return false return false
} }

View file

@ -6,7 +6,7 @@ function createMembershipEvent(membership, message) {
return simpleEvent((e) => e.type == "m.room.member" && e.content.membership === membership, message) return simpleEvent((e) => e.type == "m.room.member" && e.content.membership === membership, message)
} }
const JoinedEvent = createMembershipEvent("join", (e) => "joined the room") const JoinedEvent = createMembershipEvent("join", (e) => {console.log(e); return "joined the room"})
const InvitedEvent = createMembershipEvent("invite", (e) => `invited ${e.content.displayname} the room`) const InvitedEvent = createMembershipEvent("invite", (e) => `invited ${e.content.displayname} the room`)
const LeaveEvent = createMembershipEvent("leave", () => "left the room") const LeaveEvent = createMembershipEvent("leave", () => "left the room")

View file

@ -1,17 +1,21 @@
const {ejs, ElemJS} = require("../basic") const {ejs} = require("../basic")
const hljs = require("highlight.js")
const {HighlightedCode} = require("./components")
const DOMPurify = require("dompurify") const DOMPurify = require("dompurify")
const {resolveMxc} = require("../functions") const {resolveMxc} = require("../functions")
const {MatrixEvent} = require("./event") const {MatrixEvent} = require("./event")
const purifier = DOMPurify() const purifier = DOMPurify()
purifier.addHook("uponSanitizeAttribute", (node, hookevent, config) => { purifier.setConfig({
//If purifier already rejected an attribute there is no point in checking it ALLOWED_URI_REGEXP: /^mxc:\/\/[a-zA-Z0-9\.]+\/[a-zA-Z0-9]+$/, // As per the spec we only allow mxc uris
if (hookevent.keepAttr === false) return; ALLOWED_TAGS: ['font', 'del', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', 'sup', 'sub', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'caption', 'pre', 'span', 'img'],
const allowedElementAttributes = { // In case we mess up none of those attributes should read to XSS
ALLOWED_ATTR: ["data-mx-bg-color", "data-mx-color", "color",
"name", "target", "href",
"width", "height", "alt", "title", "src", "data-mx-emoticon",
"start", "class"],
//Custom config option that allows the array of attributes for a given tag
ALLOWED_ATTR_CUSTOM: {
"FONT": ["data-mx-bg-color", "data-mx-color", "color"], "FONT": ["data-mx-bg-color", "data-mx-color", "color"],
"SPAN": ["data-mx-bg-color", "data-mx-color", "color"], "SPAN": ["data-mx-bg-color", "data-mx-color", "color"],
"A": ["name", "target", "href"], "A": ["name", "target", "href"],
@ -19,73 +23,58 @@ purifier.addHook("uponSanitizeAttribute", (node, hookevent, config) => {
"OL": ["start"], "OL": ["start"],
"CODE": ["class"], "CODE": ["class"],
} }
})
const allowed_attributes = allowedElementAttributes[node.tagName] || [] //Handle our custom tag
purifier.addHook("uponSanitizeAttribute", (node, hookevent, config) => {
//If purifier already rejected an attribute there is no point in checking it
if (hookevent.keepAttr === false) return;
const allowed_attributes = config.ALLOWED_ATTR_CUSTOM[node.tagName] || []
hookevent.keepAttr = allowed_attributes.indexOf(hookevent.attrName) > -1; hookevent.keepAttr = allowed_attributes.indexOf(hookevent.attrName) > -1;
}) })
//Remove bad classes from our code element //Remove bad classes from our code element
purifier.addHook("uponSanitizeElement", (node, hookevent, config) => { purifier.addHook("uponSanitizeElement", (node, hookevent, config) => {
if (node.tagName == "CODE") { if (node.tagName != "CODE") return
node.classList.forEach(c => { node.classList.forEach(c => {
if (!c.startsWith("language-")) { if (!c.startsWith("language-")) {
node.classList.remove(c) node.classList.remove(c)
} }
}) })
} return node
if (node.tagName == "A") { })
purifier.addHook("afterSanitizeAttributes", (node, hookevent, config) => {
if (node.tagName == "IMG") {
let src = node.getAttribute("src")
if (src) src = resolveMxc(src)
node.setAttribute("src", src)
} else if (node.tagName == "A") {
node.setAttribute("rel", "noopener") node.setAttribute("rel", "noopener")
} else if (node.tagName == "FONT" || node.tagName == "SPAN") {
const color = node.getAttribute("data-mx-color")
const bgColor = node.getAttribute("data-mx-bg-color")
if (color) node.style.color = color;
if (bgColor) node.style.backgroundColor = bgColor;
} }
return node return node
}) })
function cleanHTML(html) {
const config = {
ALLOWED_URI_REGEXP: /^mxc:\/\/[a-zA-Z0-9\.]+\/[a-zA-Z0-9]+$/, // As per the spec we only allow mxc uris
ALLOWED_TAGS: ['font', 'del', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', 'sup', 'sub', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'caption', 'pre', 'span', 'img'],
// In case we mess up in the uponSanitizeAttribute hook function sanitize(html) {
ALLOWED_ATTR: ["data-mx-bg-color", "data-mx-color", "color",
"name", "target", "href",
"width", "height", "alt", "title", "src", "data-mx-emoticon",
"start", "class"],
}
return purifier.sanitize(html) return purifier.sanitize(html)
} }
//Here we put all the processing of the messages that isn't as likely to potentially lead to security issues
function postProcessElements(rootNode) {
const element = rootNode.element
element.querySelectorAll("code").forEach((n) => rootNode.child(new HighlightedCode(n)))
element.querySelectorAll("img").forEach((n) => {
let src = n.getAttribute("src")
if (src) src = resolveMxc(src)
n.setAttribute("src", src)
})
element.querySelectorAll("font, span").forEach((n) => {
const color = n.getAttribute("data-mx-color") || n.getAttribute("color")
const bgColor = n.getAttribute("data-mx-bg-color")
if (color) n.style.color = color;
if (bgColor) n.style.backgroundColor = bgColor;
})
}
class HTMLMessage extends MatrixEvent { class HTMLMessage extends MatrixEvent {
render() { render() {
this.clearChildren() super.render()
let html = this.data.content.formatted_body let html = this.data.content.formatted_body
const content = ejs("div") const content = ejs("div")
html = sanitize(html)
html = cleanHTML(html)
content.html(html) content.html(html)
postProcessElements(content)
this.child(content) this.child(content)
super.render()
} }
static canRender(event) { static canRender(event) {
@ -102,8 +91,8 @@ class HTMLMessage extends MatrixEvent {
class TextMessage extends MatrixEvent { class TextMessage extends MatrixEvent {
render() { render() {
this.text(this.data.content.body) super.render()
return super.render() return this.text(this.data.content.body)
} }
static canRender(event) { static canRender(event) {

View file

@ -1,3 +1,4 @@
const {simpleEvent} = require("./event") const {simpleEvent} = require("./event")
const UnknownEvent = simpleEvent(() => true, () => "Cannot render event") const UnknownEvent = simpleEvent(() => true, () => "Cannot render event")
console.log(UnknownEvent)
module.exports = [UnknownEvent] module.exports = [UnknownEvent]

View file

@ -1 +0,0 @@
@use "../../../node_modules/highlight.js/scss/obsidian"

View file

@ -5,5 +5,4 @@
@use "./components/chat" @use "./components/chat"
@use "./components/chat-input" @use "./components/chat-input"
@use "./components/anchor" @use "./components/anchor"
@use "./components/highlighted-code"
@use "./loading" @use "./loading"