Skip to content

Commit 3d27ee2

Browse files
fix(attributes): sanitize keys and values to prevent html injection
1 parent 1905567 commit 3d27ee2

File tree

3 files changed

+103
-23
lines changed

3 files changed

+103
-23
lines changed

__test__/attributes-to-string.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,5 +49,80 @@ describe('Attributes to String', () => {
4949
expect(resultString).toEqual(' style="text-align:left; " rows="4" cols="2" colWidths="250, 250"')
5050
done()
5151
})
52+
it('Should rignore attributes with forbidden characters in keys and values', done => {
53+
const attr = {
54+
"style": {
55+
"text-align": "left"
56+
},
57+
"rows": 4,
58+
"cols": 2,
59+
"colWidths": [250, 250],
60+
"<ls": "\"></p><h1>test</h1><p class=\"",
61+
"\"></p><h1>test</h1><p class=\"": 1
62+
} as Attributes;
63+
64+
const resultString = attributeToString(attr);
65+
66+
expect(resultString).toEqual(' style=\"text-align:left; \" rows=\"4\" cols=\"2\" colWidths=\"250, 250\" &lt;ls=\"&quot;&gt;&lt;/p&gt;&lt;h1&gt;test&lt;/h1&gt;&lt;p class=&quot;\"')
67+
done();
68+
});
69+
it('Should handle object attribute values correctly', done => {
70+
const attr = {
71+
"style": {
72+
"color": "red",
73+
"font-size": "14px"
74+
}
75+
} as Attributes;
76+
77+
const resultString = attributeToString(attr);
78+
79+
expect(resultString).toEqual(' style="color:red; font-size:14px; "');
80+
done();
81+
});
82+
it('Should convert arrays into comma-separated values', done => {
83+
const attr = {
84+
"data-values": [10, 20, 30]
85+
} as Attributes;
86+
87+
const resultString = attributeToString(attr);
88+
89+
expect(resultString).toEqual(' data-values="10, 20, 30"');
90+
done();
91+
});
92+
it('Should handle special characters in values properly', done => {
93+
const attr = {
94+
"title": 'This & That > Those < Them "Quoted"',
95+
"description": "Hello <script>alert(xss)</script>"
96+
} as Attributes;
97+
98+
const resultString = attributeToString(attr);
99+
100+
expect(resultString).toEqual(' title="This &amp; That &gt; Those &lt; Them &quot;Quoted&quot;" description="Hello &lt;script&gt;alert(xss)&lt;/script&gt;"');
101+
done();
102+
});
103+
104+
it('Should handle mixed types of values properly', done => {
105+
const attr = {
106+
"rows": 5,
107+
"isEnabled": true,
108+
"ids": [101, 102],
109+
"style": { "margin": "10px", "padding": "5px" }
110+
} as Attributes;
111+
112+
const resultString = attributeToString(attr);
113+
114+
expect(resultString).toEqual(' rows="5" isEnabled="true" ids="101, 102" style="margin:10px; padding:5px; "');
115+
done();
116+
});
117+
it('Should sanitize both keys and values to prevent HTML injection', done => {
118+
const attr = {
119+
"<script>alert('key')</script>": "test",
120+
"safeKey": "<script>alert(xss)</script>"
121+
} as Attributes;
122+
123+
const resultString = attributeToString(attr);
52124

125+
expect(resultString).toEqual(' safeKey="&lt;script&gt;alert(xss)&lt;/script&gt;"');
126+
done();
127+
});
53128
})

src/Models/metadata-model.ts

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import StyleType from '../embedded-types/style-type';
22
import TextNode from '../nodes/text-node';
3+
import { replaceHtmlEntities, forbiddenAttrChars } from '../helper/enumerate-entries';
4+
35
export interface Metadata {
46
text: string;
57
attributes: Attributes;
@@ -58,30 +60,30 @@ export function attributeToString(attributes: Attributes): string {
5860
let result = '';
5961
for (const key in attributes) {
6062
if (Object.prototype.hasOwnProperty.call(attributes, key)) {
61-
let element = attributes[key];
62-
if (element instanceof Array) {
63-
let elementString = '';
64-
let isFirst = true;
65-
element.forEach((value) => {
66-
if (isFirst) {
67-
elementString += `${value}`;
68-
isFirst = false;
69-
} else {
70-
elementString += `, ${value}`;
71-
}
72-
});
73-
element = elementString;
74-
} else if (typeof element === 'object') {
63+
// Sanitize the key to prevent HTML injection
64+
const sanitizedKey = replaceHtmlEntities(key);
65+
66+
// Skip keys that contain forbidden characters (even after sanitization)
67+
if (forbiddenAttrChars.some(char => sanitizedKey.includes(char))) {
68+
continue;
69+
}
70+
let value = attributes[key];
71+
if (Array.isArray(value)) {
72+
value = value.join(', ');
73+
} else if (typeof value === 'object') {
7574
let elementString = '';
76-
for (const elementKey in element) {
77-
if (Object.prototype.hasOwnProperty.call(element, elementKey)) {
78-
const value = element[elementKey];
79-
elementString += `${elementKey}:${value}; `;
75+
for (const subKey in value) {
76+
if (Object.prototype.hasOwnProperty.call(value, subKey)) {
77+
const subValue = value[subKey];
78+
if (subValue != null && subValue !== '') {
79+
elementString += `${replaceHtmlEntities(subKey)}:${replaceHtmlEntities(String(subValue))}; `;
80+
}
8081
}
8182
}
82-
element = elementString;
83+
value = elementString;
8384
}
84-
result += ` ${key}="${element}"`;
85+
// Sanitize the value to prevent HTML injection
86+
result += ` ${sanitizedKey}="${replaceHtmlEntities(String(value))}"`;
8587
}
8688
}
8789
return result;

src/helper/enumerate-entries.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export function enumerateContents(
4242
}
4343

4444
export function textNodeToHTML(node: TextNode, renderOption: RenderOption): string {
45-
let text = escapeHtml(node.text);
45+
let text = replaceHtmlEntities(node.text);
4646
if (node.classname || node.id) {
4747
text = (renderOption[MarkType.CLASSNAME_OR_ID] as RenderMark)(text, node.classname, node.id);
4848
}
@@ -159,9 +159,12 @@ function nodeToHTML(
159159
}
160160
}
161161

162-
function escapeHtml(text: string): string {
162+
export function replaceHtmlEntities(text: string): string {
163163
return text
164164
.replace(/&/g, '&amp;')
165165
.replace(/</g, '&lt;')
166166
.replace(/>/g, '&gt;')
167-
}
167+
.replace(/"/g, '&quot;')
168+
}
169+
170+
export const forbiddenAttrChars = ['"', "'", '>','<', '/', '='];

0 commit comments

Comments
 (0)