@ecency/render-helper 2.2.39 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/methods/a.method.js +13 -1
- package/lib/methods/a.method.js.map +1 -1
- package/lib/methods/img.method.js +27 -8
- package/lib/methods/img.method.js.map +1 -1
- package/lib/methods/sanitize-html.method.js +14 -5
- package/lib/methods/sanitize-html.method.js.map +1 -1
- package/lib/render-helper.js +1 -1
- package/package.json +1 -1
- package/src/methods/a.method.ts +31 -12
- package/src/methods/img.method.ts +36 -9
- package/src/methods/sanitize-html.method.ts +16 -9
- package/src/test/data/legacy/10.json +1 -1
- package/src/test/data/legacy/21.json +1 -1
- package/src/test/data/legacy/22.json +1 -1
- package/src/test/data/legacy/23.json +1 -1
- package/src/test/data/legacy/26.json +1 -1
package/package.json
CHANGED
package/src/methods/a.method.ts
CHANGED
|
@@ -32,6 +32,11 @@ import { proxifyImageSrc } from '../proxify-image-src'
|
|
|
32
32
|
import { removeChildNodes } from './remove-child-nodes.method'
|
|
33
33
|
import { extractYtStartTime } from '../helper'
|
|
34
34
|
|
|
35
|
+
function isValidPermlink(permlink: string): boolean {
|
|
36
|
+
// Reject anything that looks like a file (ends with .jpg/.png/.webp/.jpeg etc)
|
|
37
|
+
return !/\.(jpg|jpeg|png|webp|gif|svg)$/i.test(permlink);
|
|
38
|
+
}
|
|
39
|
+
|
|
35
40
|
export function a(el: HTMLElement, forApp: boolean, webp: boolean): void {
|
|
36
41
|
let href = el.getAttribute('href')
|
|
37
42
|
|
|
@@ -101,6 +106,9 @@ export function a(el: HTMLElement, forApp: boolean, webp: boolean): void {
|
|
|
101
106
|
const tag = postMatch[2]
|
|
102
107
|
const author = postMatch[3].replace('@', '')
|
|
103
108
|
const permlink = postMatch[4]
|
|
109
|
+
|
|
110
|
+
if (!isValidPermlink(permlink)) return;
|
|
111
|
+
|
|
104
112
|
let isInline = true;
|
|
105
113
|
if (el.textContent === href) {
|
|
106
114
|
el.textContent = `@${author}/${permlink}`
|
|
@@ -133,7 +141,7 @@ export function a(el: HTMLElement, forApp: boolean, webp: boolean): void {
|
|
|
133
141
|
}
|
|
134
142
|
if (forApp) {
|
|
135
143
|
el.removeAttribute('href')
|
|
136
|
-
|
|
144
|
+
|
|
137
145
|
el.setAttribute('data-author', author)
|
|
138
146
|
} else {
|
|
139
147
|
const h = `/@${author}`
|
|
@@ -180,6 +188,9 @@ export function a(el: HTMLElement, forApp: boolean, webp: boolean): void {
|
|
|
180
188
|
el.setAttribute('class', 'markdown-post-link')
|
|
181
189
|
const author = tpostMatch[2].replace('@', '')
|
|
182
190
|
const permlink = tpostMatch[3]
|
|
191
|
+
|
|
192
|
+
if (!isValidPermlink(permlink)) return;
|
|
193
|
+
|
|
183
194
|
let isInline = true;
|
|
184
195
|
if (el.textContent === href) {
|
|
185
196
|
el.textContent = `@${author}/${permlink}`
|
|
@@ -252,7 +263,11 @@ export function a(el: HTMLElement, forApp: boolean, webp: boolean): void {
|
|
|
252
263
|
|
|
253
264
|
const author = cpostMatch[1].replace('@', '')
|
|
254
265
|
const permlink = cpostMatch[2]
|
|
266
|
+
|
|
267
|
+
if (!isValidPermlink(permlink)) return;
|
|
268
|
+
|
|
255
269
|
let isInline = true;
|
|
270
|
+
|
|
256
271
|
if (el.textContent === href) {
|
|
257
272
|
el.textContent = `@${author}/${permlink}`
|
|
258
273
|
isInline = false;
|
|
@@ -353,7 +368,11 @@ export function a(el: HTMLElement, forApp: boolean, webp: boolean): void {
|
|
|
353
368
|
const tag = 'ccc'
|
|
354
369
|
const author = cccMatch[2].replace('@', '')
|
|
355
370
|
const permlink = cccMatch[3]
|
|
371
|
+
|
|
372
|
+
if (!isValidPermlink(permlink)) return;
|
|
373
|
+
|
|
356
374
|
let isInline = true;
|
|
375
|
+
|
|
357
376
|
if (el.textContent === href) {
|
|
358
377
|
el.textContent = `@${author}/${permlink}`
|
|
359
378
|
isInline = false;
|
|
@@ -441,7 +460,7 @@ export function a(el: HTMLElement, forApp: boolean, webp: boolean): void {
|
|
|
441
460
|
const embedSrc = `https://www.youtube.com/embed/${vid}?autoplay=1`
|
|
442
461
|
|
|
443
462
|
el.textContent = ''
|
|
444
|
-
|
|
463
|
+
|
|
445
464
|
el.setAttribute('data-embed-src', embedSrc);
|
|
446
465
|
el.setAttribute('data-youtube', vid);
|
|
447
466
|
|
|
@@ -521,7 +540,7 @@ export function a(el: HTMLElement, forApp: boolean, webp: boolean): void {
|
|
|
521
540
|
if (match && el.textContent.trim() === href) {
|
|
522
541
|
const e = SPOTIFY_REGEX.exec(href)
|
|
523
542
|
if (e[1]) {
|
|
524
|
-
|
|
543
|
+
|
|
525
544
|
el.setAttribute('class', 'markdown-audio-link markdown-audio-link-spotify')
|
|
526
545
|
el.removeAttribute('href')
|
|
527
546
|
|
|
@@ -536,7 +555,7 @@ export function a(el: HTMLElement, forApp: boolean, webp: boolean): void {
|
|
|
536
555
|
ifr.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-popups')
|
|
537
556
|
el.appendChild(ifr)
|
|
538
557
|
|
|
539
|
-
return
|
|
558
|
+
return
|
|
540
559
|
}
|
|
541
560
|
}
|
|
542
561
|
|
|
@@ -545,7 +564,7 @@ export function a(el: HTMLElement, forApp: boolean, webp: boolean): void {
|
|
|
545
564
|
if (match && el.textContent.trim() === href) {
|
|
546
565
|
const e = LOOM_REGEX.exec(href)
|
|
547
566
|
if (e[2]) {
|
|
548
|
-
|
|
567
|
+
|
|
549
568
|
el.setAttribute('class', 'markdown-video-link markdown-video-link-loom')
|
|
550
569
|
el.removeAttribute('href')
|
|
551
570
|
|
|
@@ -560,7 +579,7 @@ export function a(el: HTMLElement, forApp: boolean, webp: boolean): void {
|
|
|
560
579
|
ifr.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-popups')
|
|
561
580
|
el.appendChild(ifr)
|
|
562
581
|
|
|
563
|
-
return
|
|
582
|
+
return
|
|
564
583
|
}
|
|
565
584
|
}
|
|
566
585
|
|
|
@@ -577,21 +596,21 @@ export function a(el: HTMLElement, forApp: boolean, webp: boolean): void {
|
|
|
577
596
|
if (e[2] && e[3]) {
|
|
578
597
|
el.setAttribute('class', 'markdown-video-link markdown-video-link-dtube')
|
|
579
598
|
el.removeAttribute('href')
|
|
580
|
-
|
|
599
|
+
|
|
581
600
|
|
|
582
601
|
const videoHref = `https://emb.d.tube/#!/${e[2]}/${e[3]}`
|
|
583
602
|
|
|
584
603
|
// el.setAttribute('data-video-href', videoHref)
|
|
585
604
|
el.setAttribute('data-embed-src', videoHref)
|
|
586
605
|
|
|
587
|
-
//process thumb img element
|
|
606
|
+
//process thumb img element
|
|
588
607
|
if (imgEls.length === 1) {
|
|
589
|
-
const thumbnail = proxifyImageSrc(imgEls[0].getAttribute('src').replace(/\s+/g, ''), 0, 0, webp ? 'webp' : 'match')
|
|
608
|
+
const thumbnail = proxifyImageSrc(imgEls[0].getAttribute('src').replace(/\s+/g, ''), 0, 0, webp ? 'webp' : 'match')
|
|
590
609
|
const thumbImg = el.ownerDocument.createElement('img')
|
|
591
610
|
|
|
592
611
|
thumbImg.setAttribute('class', 'no-replace video-thumbnail')
|
|
593
612
|
thumbImg.setAttribute('itemprop', 'thumbnailUrl')
|
|
594
|
-
|
|
613
|
+
|
|
595
614
|
thumbImg.setAttribute('src', thumbnail)
|
|
596
615
|
el.appendChild(thumbImg)
|
|
597
616
|
|
|
@@ -604,7 +623,7 @@ export function a(el: HTMLElement, forApp: boolean, webp: boolean): void {
|
|
|
604
623
|
const play = el.ownerDocument.createElement('span')
|
|
605
624
|
play.setAttribute('class', 'markdown-video-play')
|
|
606
625
|
|
|
607
|
-
|
|
626
|
+
|
|
608
627
|
el.appendChild(play)
|
|
609
628
|
|
|
610
629
|
return
|
|
@@ -628,7 +647,7 @@ export function a(el: HTMLElement, forApp: boolean, webp: boolean): void {
|
|
|
628
647
|
play.setAttribute('class', 'markdown-video-play')
|
|
629
648
|
|
|
630
649
|
el.appendChild(play)
|
|
631
|
-
|
|
650
|
+
|
|
632
651
|
|
|
633
652
|
return
|
|
634
653
|
}
|
|
@@ -1,16 +1,43 @@
|
|
|
1
|
-
import { proxifyImageSrc } from
|
|
1
|
+
import { proxifyImageSrc } from "../proxify-image-src";
|
|
2
2
|
|
|
3
3
|
export function img(el: HTMLElement, webp: boolean): void {
|
|
4
|
-
el.removeAttribute(
|
|
5
|
-
el.removeAttribute(
|
|
4
|
+
el.removeAttribute("width");
|
|
5
|
+
el.removeAttribute("height");
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
const src = el.getAttribute("src") || "";
|
|
8
|
+
|
|
9
|
+
// Normalize encoded characters
|
|
10
|
+
const decodedSrc = decodeURIComponent(
|
|
11
|
+
src.replace(/&#(\d+);/g, (_, dec) => String.fromCharCode(dec))
|
|
12
|
+
.replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
|
|
13
|
+
).trim().toLowerCase();
|
|
14
|
+
|
|
15
|
+
// ❌ Remove if javascript or empty/invalid
|
|
16
|
+
const isInvalid = !src || decodedSrc.startsWith("javascript") || decodedSrc.startsWith("vbscript") || decodedSrc === "x";
|
|
17
|
+
if (isInvalid) {
|
|
18
|
+
el.remove();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ❌ Skip relative paths (e.g., `photo.jpg`)
|
|
23
|
+
const isRelative = !/^https?:\/\//i.test(src) && !src.startsWith("/");
|
|
24
|
+
if (isRelative) {
|
|
25
|
+
console.warn("Skipped relative image:", src);
|
|
26
|
+
el.remove();
|
|
27
|
+
return;
|
|
10
28
|
}
|
|
11
|
-
el.setAttribute('itemprop', 'image')
|
|
12
29
|
|
|
13
|
-
|
|
14
|
-
|
|
30
|
+
// Sanitize any dynamic or low-res src-like attributes
|
|
31
|
+
["onerror", "dynsrc", "lowsrc"].forEach(attr => el.removeAttribute(attr));
|
|
32
|
+
|
|
33
|
+
el.setAttribute("itemprop", "image");
|
|
34
|
+
|
|
35
|
+
const cls = el.getAttribute("class") || "";
|
|
36
|
+
const shouldReplace = !cls.includes("no-replace");
|
|
37
|
+
const hasAlreadyProxied = src.startsWith("https://images.ecency.com");
|
|
38
|
+
|
|
39
|
+
if (shouldReplace && !hasAlreadyProxied) {
|
|
40
|
+
const proxified = proxifyImageSrc(src, 0, 0, webp ? "webp" : "match");
|
|
41
|
+
el.setAttribute("src", proxified);
|
|
15
42
|
}
|
|
16
43
|
}
|
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
import xss from 'xss'
|
|
2
2
|
import { ALLOWED_ATTRIBUTES } from '../consts'
|
|
3
3
|
|
|
4
|
+
const decodeEntities = (input: string): string =>
|
|
5
|
+
input
|
|
6
|
+
.replace(/&#(\d+);?/g, (_, dec) => String.fromCharCode(dec))
|
|
7
|
+
.replace(/&#x([0-9a-f]+);?/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
8
|
+
|
|
4
9
|
export function sanitizeHtml(html: string): string {
|
|
5
10
|
return xss(html, {
|
|
6
11
|
whiteList: ALLOWED_ATTRIBUTES,
|
|
7
|
-
stripIgnoreTag: true,
|
|
8
|
-
css: true,
|
|
12
|
+
stripIgnoreTag: true,
|
|
9
13
|
stripIgnoreTagBody: ['style'],
|
|
14
|
+
css: false, // block style attrs entirely for safety
|
|
10
15
|
onTagAttr: (tag, name, value) => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (tag === 'img' && name === 'src' && !/^https?:\/\//.test(
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
const decoded = decodeEntities(value.trim().toLowerCase());
|
|
17
|
+
|
|
18
|
+
if (name.startsWith('on')) return ''; // 🛡 event handlers
|
|
19
|
+
if (tag === 'img' && name === 'src' && (!/^https?:\/\//.test(decoded) || decoded.startsWith('javascript:'))) return '';
|
|
20
|
+
if (tag === 'img' && ['dynsrc', 'lowsrc'].includes(name)) return '';
|
|
21
|
+
if (tag === 'span' && name === 'class' && value === 'wr') return '';
|
|
22
|
+
|
|
23
|
+
return undefined;
|
|
17
24
|
}
|
|
18
|
-
})
|
|
25
|
+
});
|
|
19
26
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": 10,
|
|
3
3
|
"input": "Lorem ipsum dolor <img src=x onerror=alert(x)> sit amet.\n\n<a href=\"javascript:void(0)\">etiam ut sollicitudin neque</a>\n\n<a onclick=\"console.log('ss')\">Vivamus pulvinar semper porttitor</a>",
|
|
4
|
-
"result": "
|
|
4
|
+
"result": ""
|
|
5
5
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": 21,
|
|
3
3
|
"input": "javascript:/*--></title></style></textarea></script></xmp><svg/onload='+/\"/+/onmouseover=1/+/[*/[]/+alert(1)//'>\n\n<IMG SRC=\"javascript:alert('XSS');\"><img src=\"javascript:alert('XSS');\">\n\n<IMG SRC=javascript:alert('XSS')> <img src=javascript:alert('XSS')>",
|
|
4
|
-
"result": "
|
|
4
|
+
"result": ""
|
|
5
5
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": 22,
|
|
3
3
|
"input": "<IMG SRC=JaVaScRiPt:alert('XSS')> <IMG SRC=javascript:alert("XSS")>\n<IMG SRC=`javascript:alert(\"RSnake says, 'XSS'\")`>\n<a onmouseover=\"alert(document.cookie)\">xxs link</a><a onmouseover=alert(document.cookie)>xxs link</a>\n<IMG \"\"\"><SCRIPT>alert(\"XSS\")</SCRIPT>\"><IMG SRC=javascript:alert(String.fromCharCode(88,83,83))>\n <IMG SRC= onmouseover=\"alert('xxs')\">",
|
|
4
|
-
"result": "
|
|
4
|
+
"result": ""
|
|
5
5
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": 23,
|
|
3
3
|
"input": "<IMG SRC=/ onerror=\"alert(String.fromCharCode(88,83,83))\"></img> <img src=x onerror=\"javascript:alert('XSS')\"> <IMG SRC=javascript:alert('XSS')> <IMG SRC=javascript:alert('XSS')> <IMG SRC=\"  javascript:alert('XSS');\">",
|
|
4
|
-
"result": "
|
|
4
|
+
"result": ""
|
|
5
5
|
}
|