Skip to content

Commit 522b172

Browse files
authored
Merge pull request #30 from sirenkovladd/escaping-tags
add escaping title and meta tags, fixes #21
2 parents a6bf3be + 796cf3b commit 522b172

File tree

2 files changed

+61
-15
lines changed

2 files changed

+61
-15
lines changed

src/index.tsx

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const MetaContext = createContext<MetaContextType>();
1414
interface TagDescription {
1515
tag: string;
1616
props: Record<string, unknown>;
17+
setting?: { escape?: boolean };
1718
id: string;
1819
name?: string;
1920
ref?: Element;
@@ -174,14 +175,15 @@ const MetaProvider: ParentComponent<{ tags?: Array<TagDescription> }> = props =>
174175
return <MetaContext.Provider value={actions}>{props.children}</MetaContext.Provider>;
175176
};
176177

177-
const MetaTag = (tag: string, props: { [k: string]: any }) => {
178+
const MetaTag = (tag: string, props: { [k: string]: any }, setting?: { escape?: boolean }) => {
178179
const id = createUniqueId();
179180
const c = useContext(MetaContext);
180181
if (!c) throw new Error("<MetaProvider /> should be in the tree");
181182

182183
useHead({
183184
tag,
184185
props,
186+
setting,
185187
id,
186188
get name() {
187189
return props.name || props.property;
@@ -193,12 +195,7 @@ const MetaTag = (tag: string, props: { [k: string]: any }) => {
193195

194196
export { MetaProvider };
195197

196-
export function useHead(tagDesc: {
197-
tag: string;
198-
props: { [k: string]: any };
199-
id: string;
200-
name: any;
201-
}) {
198+
export function useHead(tagDesc: TagDescription) {
202199
const { addClientTag, removeClientTag, addServerTag } = useContext(MetaContext)!;
203200

204201
createRenderEffect(() => {
@@ -214,30 +211,44 @@ export function useHead(tagDesc: {
214211
}
215212
}
216213

214+
function escapeHTML(html: string) {
215+
return html.replace(/</g, "&lt;").replace(/>/g, "&gt;");
216+
}
217+
218+
function escapeString(str: any) {
219+
if (typeof str === "string") {
220+
return str.replace(/"/g, '&quot;');
221+
}
222+
return str;
223+
}
224+
217225
export function renderTags(tags: Array<TagDescription>) {
218226
return tags
219227
.map(tag => {
220228
const keys = Object.keys(tag.props);
221-
const props = keys.map(k => (k === "children" ? "" : ` ${k}="${tag.props[k]}"`)).join("");
222-
return tag.props.children
223-
? `<${tag.tag} data-sm="${tag.id}"${props}>${
229+
const props = keys.map(k => (k === "children" ? "" : ` ${k}="${escapeString(tag.props[k])}`)).join("");
230+
if (tag.props.children) {
224231
// Tags might contain multiple text children:
225232
// <Title>example - {myCompany}</Title>
226-
Array.isArray(tag.props.children) ? tag.props.children.join("") : tag.props.children
227-
}</${tag.tag}>`
228-
: `<${tag.tag} data-sm="${tag.id}"${props}/>`;
233+
const children = Array.isArray(tag.props.children) ? tag.props.children.join("") : tag.props.children;
234+
if (tag.setting?.escape && typeof children === "string") {
235+
return `<${tag.tag} data-sm="${tag.id}"${props}>${escapeHTML(children)}</${tag.tag}>`;
236+
}
237+
return `<${tag.tag} data-sm="${tag.id}"${props}>${children}</${tag.tag}>`;
238+
}
239+
return `<${tag.tag} data-sm="${tag.id}"${props}/>`;
229240
})
230241
.join("");
231242
}
232243

233244
export const Title: Component<JSX.HTMLAttributes<HTMLTitleElement>> = props =>
234-
MetaTag("title", props);
245+
MetaTag("title", props, { escape: true });
235246

236247
export const Style: Component<JSX.StyleHTMLAttributes<HTMLStyleElement>> = props =>
237248
MetaTag("style", props);
238249

239250
export const Meta: Component<JSX.MetaHTMLAttributes<HTMLMetaElement>> = props =>
240-
MetaTag("meta", props);
251+
MetaTag("meta", props, { escape: true });
241252

242253
export const Link: Component<JSX.LinkHTMLAttributes<HTMLLinkElement>> = props =>
243254
MetaTag("link", props);

test/index.spec.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,3 +273,38 @@ test("throws error if head tag is rendered without MetaProvider", () => {
273273
render(() => <Style>{`body {}`}</Style>, div);
274274
}).toThrowError(/<MetaProvider \/> should be in the tree/);
275275
});
276+
277+
test("Escaping the title tag", () => {
278+
let div = document.createElement("div");
279+
const snapshot = '<title>Hello&lt;/title&gt;&lt;script&gt;alert("inject");&lt;/script&gt;&lt;title&gt; World</title>';
280+
const dispose = render(
281+
() => (
282+
<MetaProvider>
283+
<div>
284+
<Title>{'Hello</title><script>alert("inject");</script><title> World'}</Title>
285+
</div>
286+
</MetaProvider>
287+
),
288+
div
289+
);
290+
expect(document.head.innerHTML).toBe(snapshot);
291+
dispose();
292+
});
293+
294+
test("Escaping the title meta", () => {
295+
let div = document.createElement("div");
296+
const snapshot = '<meta content="Text in &quot;quotes&quot;">';
297+
298+
const dispose = render(
299+
() => (
300+
<MetaProvider>
301+
<div>
302+
<Meta content={'Text in "quotes"'} />
303+
</div>
304+
</MetaProvider>
305+
),
306+
div
307+
);
308+
expect(document.head.innerHTML).toBe(snapshot);
309+
dispose();
310+
});

0 commit comments

Comments
 (0)