Skip to content

Commit 0d9a245

Browse files
JeanMechealxhub
authored andcommitted
fix(core): sanitize meta selectors
Ensure that property/name are correctly escaped and doesn't break out of the intended selector. (cherry picked from commit d5a489a)
1 parent 2200b4a commit 0d9a245

2 files changed

Lines changed: 69 additions & 3 deletions

File tree

packages/platform-browser/src/browser/meta.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,8 @@ export class Meta {
102102
*/
103103
getTag(attrSelector: string): HTMLMetaElement | null {
104104
if (!attrSelector) return null;
105-
return this._doc.querySelector(`meta[${attrSelector}]`) || null;
105+
const meta = this._doc.querySelector(`meta[${attrSelector}]`);
106+
return meta?.nodeName.toLowerCase() === 'meta' ? meta : null;
106107
}
107108

108109
/**
@@ -114,7 +115,11 @@ export class Meta {
114115
getTags(attrSelector: string): HTMLMetaElement[] {
115116
if (!attrSelector) return [];
116117
const list /*NodeList*/ = this._doc.querySelectorAll(`meta[${attrSelector}]`);
117-
return list ? [].slice.call(list) : [];
118+
return list
119+
? (([].slice.call(list) as HTMLElement[]).filter(
120+
(elem) => elem.nodeName.toLowerCase() === 'meta',
121+
) as HTMLMetaElement[])
122+
: [];
118123
}
119124

120125
/**
@@ -183,7 +188,13 @@ export class Meta {
183188

184189
private _parseSelector(tag: MetaDefinition): string {
185190
const attr: string = tag.name ? 'name' : 'property';
186-
return `${attr}="${tag[attr]}"`;
191+
return `${attr}=${this._escapeSelectorValue(String(tag[attr]))}`;
192+
}
193+
194+
private _escapeSelectorValue(value: string): string {
195+
// Escape backslashes and double quotes to prevent CSS selector injection.
196+
// This securely confines the value inside an attribute selector.
197+
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
187198
}
188199

189200
private _containsAttributes(tag: MetaDefinition, elem: HTMLMetaElement): boolean {

packages/platform-browser/test/browser/meta_spec.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,24 @@ describe('Meta service', () => {
8989
expect(actual!.getAttribute('content')).toEqual('4321');
9090
});
9191

92+
it('should not allow a custom selector to match off target elements like the body tag', () => {
93+
// This payload attempts to break out of the `meta[name="..."]` constraint entirely
94+
// and inject a comma to target arbitrary DOM elements like the `body` tag via the
95+
// `selector` argument of `updateTag`.
96+
const attackerSelector = 'name="description"], body, meta[name="pwned"';
97+
98+
const firstMeta = metaService.updateTag({content: 'pwned'}, attackerSelector)!;
99+
100+
expect(firstMeta).not.toBeNull();
101+
// It creates a new meta element instead of targeting `body` because it did not
102+
// find a meta element matching the dirty selector since `body` is not a `meta` tag
103+
expect(firstMeta!.nodeName.toLowerCase()).toEqual('meta');
104+
expect(firstMeta!.getAttribute('content')).toEqual('pwned');
105+
expect(doc.body.getAttribute('content')).toBeNull();
106+
107+
metaService.removeTagElement(firstMeta);
108+
});
109+
92110
it('should extract selector from the tag definition', () => {
93111
const selector = 'property="fb:app_id"';
94112
metaService.updateTag({property: 'fb:app_id', content: '666'});
@@ -137,6 +155,43 @@ describe('Meta service', () => {
137155
metaService.removeTagElement(actual);
138156
});
139157

158+
it('should escape selector values when deriving the match selector', () => {
159+
// This payload attempts to prematurely close the attribute selector
160+
// and match another attribute.
161+
const property = 'fb:app_id"][content="123456789';
162+
163+
const meta = metaService.updateTag({property, content: 'pwned'})!;
164+
165+
expect(meta).not.toBe(defaultMeta);
166+
expect(meta.getAttribute('property')).toEqual(property);
167+
expect(meta.getAttribute('content')).toEqual('pwned');
168+
expect(metaService.getTags('property="fb:app_id"').length).toEqual(1);
169+
170+
// clean up
171+
metaService.removeTagElement(meta);
172+
});
173+
174+
it('should not let a quoted name break out of the meta selector and target body', () => {
175+
// This payload attempts to break out of the `meta[name="..."]` constraint entirely
176+
// and inject a comma to target arbitrary DOM elements like the `body` tag.
177+
const attackerName = 'description"], body';
178+
179+
const firstMeta = metaService.addTag({name: attackerName, content: 'safe'})!;
180+
const secondMeta = metaService.addTag({name: attackerName, content: 'safe'})!;
181+
182+
expect(firstMeta).toBe(secondMeta);
183+
expect(firstMeta.tagName).toEqual('META');
184+
expect(
185+
Array.from(doc.getElementsByTagName('meta')).filter(
186+
(meta) => meta.getAttribute('name') === attackerName,
187+
).length,
188+
).toEqual(1);
189+
expect(doc.body).not.toBeNull();
190+
191+
// clean up
192+
metaService.removeTagElement(firstMeta);
193+
});
194+
140195
it('should add multiple new meta tags', () => {
141196
const nameSelector = 'name="twitter:title"';
142197
const propertySelector = 'property="og:title"';

0 commit comments

Comments
 (0)