@ecency/render-helper 2.2.40 → 2.3.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecency/render-helper",
3
- "version": "2.2.40",
3
+ "version": "2.3.1",
4
4
  "description": "Markdown+Html Render helper",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -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
  }
@@ -28,4 +28,5 @@ export function cleanReply(s: string): string {
28
28
  .replace('Posted via <a href="https://d.buzz" data-link="promote-link">D.Buzz</a>', '')
29
29
  .replace('<div class="pull-right"><a href="/@hive.engage">![](https://i.imgur.com/XsrNmcl.png)</a></div>', '')
30
30
  .replace('<div><a href="https://engage.hivechain.app">![](https://i.imgur.com/XsrNmcl.png)</a></div>', '')
31
+ .replace(`<div class="text-center"><img src="https://cdn.steemitimages.com/DQmNp6YwAm2qwquALZw8PdcovDorwaBSFuxQ38TrYziGT6b/A-20.png"><a href="https://bit.ly/actifit-app"><img src="https://cdn.steemitimages.com/DQmQqfpSmcQtfrHAtzfBtVccXwUL9vKNgZJ2j93m8WNjizw/l5.png"></a><a href="https://bit.ly/actifit-ios"><img src="https://cdn.steemitimages.com/DQmbWy8KzKT1UvCvznUTaFPw6wBUcyLtBT5XL9wdbB7Hfmn/l6.png"></a></div>`, '')
31
32
  }
@@ -4,19 +4,40 @@ export function img(el: HTMLElement, webp: boolean): void {
4
4
  el.removeAttribute("width");
5
5
  el.removeAttribute("height");
6
6
 
7
- let src = el.getAttribute("src");
8
- if (src.startsWith("javascript")) {
9
- src = "";
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
  }
29
+
30
+ // Sanitize any dynamic or low-res src-like attributes
31
+ ["onerror", "dynsrc", "lowsrc"].forEach(attr => el.removeAttribute(attr));
32
+
11
33
  el.setAttribute("itemprop", "image");
12
34
 
13
- const hasAlreadyProxied =
14
- el.getAttribute("src")?.startsWith("https://images.ecency.com") === true;
35
+ const cls = el.getAttribute("class") || "";
36
+ const shouldReplace = !cls.includes("no-replace");
37
+ const hasAlreadyProxied = src.startsWith("https://images.ecency.com");
15
38
 
16
- if (
17
- el.getAttribute("class").indexOf("no-replace") === -1 &&
18
- !hasAlreadyProxied
19
- ) {
20
- el.setAttribute("src", proxifyImageSrc(src, 0, 0, webp ? "webp" : "match"));
39
+ if (shouldReplace && !hasAlreadyProxied) {
40
+ const proxified = proxifyImageSrc(src, 0, 0, webp ? "webp" : "match");
41
+ el.setAttribute("src", proxified);
21
42
  }
22
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, // filter out all HTML not in the whitelist
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
- if (tag === 'span' && name === 'class' && value === 'wr') {
12
- return ''
13
- }
14
- if (tag === 'img' && name === 'src' && !/^https?:\/\//.test(value)) {
15
- return ''
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": "<p dir=\"auto\">Lorem ipsum dolor <img /> sit amet.</p>\n<p dir=\"auto\"><a>etiam ut sollicitudin neque</a></p>\n<p dir=\"auto\"><a>Vivamus pulvinar semper porttitor</a></p>"
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": "<p dir=\"auto\">javascript:/<em>--&gt;&lt;svg/onload='+/\"/+/onmouseover=1/+/[</em>/[]/+alert(1)//'&gt;</p>\n<p dir=\"auto\"><img /><img /></p>\n<p dir=\"auto\">&lt;IMG SRC=javascript:alert('XSS')&gt; &lt;img src=javascript:alert('XSS')&gt;</p>"
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(&quot;XSS&quot;)>\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": "<p dir=\"auto\">&lt;IMG SRC=JaVaScRiPt:alert('XSS')&gt; &lt;IMG SRC=javascript:alert(&quot;XSS&quot;)&gt;<br />\n&lt;IMG SRC=<code>javascript:alert(\"RSnake says, 'XSS'\")</code>&gt;<br />\n<a>xxs link</a><a>xxs link</a><br />\n&lt;IMG \"\"\"&gt;alert(&quot;XSS&quot;)\"&gt;<img /><br />\n&lt;IMG SRC= onmouseover=\"alert('xxs')\"&gt;</p>"
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=\"&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041\"> <IMG SRC=&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;&#108;&#101;&#114;&#116;&#40;&#39;&#88;&#83;&#83;&#39;&#41;> <IMG SRC=&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29> <IMG SRC=\" &#14; javascript:alert('XSS');\">",
4
- "result": "<p dir=\"auto\">&lt;IMG SRC=/ onerror=\"alert(String.fromCharCode(88,83,83))\"&gt; <img /> &lt;IMG SRC=&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;&#108;&#101;&#114;&#116;&#40;&#39;&#88;&#83;&#83;&#39;&#41;&gt; <img /> <img /></p>"
4
+ "result": ""
5
5
  }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "id": 26,
3
3
  "input": "<IMG DYNSRC=\"javascript:alert('XSS')\"> <IMG LOWSRC=\"javascript:alert('XSS')\"> <STYLE>li {list-style-image: url(\"javascript:alert('XSS')\");}</STYLE>",
4
- "result": "<p dir=\"auto\"><img /> <img /> </p>"
4
+ "result": ""
5
5
  }