Skip to content

Commit de0eb4c

Browse files
crisbetomattrbeck
authored andcommitted
fix(core): sanitize translated form attributes
Fixes that we weren't sanitizing the `form` and `formaction` attributes when they're used together with translations.
1 parent 999c14e commit de0eb4c

File tree

4 files changed

+61
-11
lines changed

4 files changed

+61
-11
lines changed

packages/core/src/render3/i18n/i18n_parse.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import '../../util/ng_i18n_closure_mode';
1111
import {XSS_SECURITY_URL} from '../../error_details_base_url';
1212
import {
1313
getTemplateContent,
14-
URI_ATTRS,
14+
SENSITIVE_ATTRS,
1515
VALID_ATTRS,
1616
VALID_ELEMENTS,
1717
} from '../../sanitization/html_sanitizer';
@@ -388,7 +388,7 @@ export function i18nAttributesFirstPass(tView: TView, index: number, values: str
388388
previousElementIndex,
389389
attrName,
390390
countBindings(updateOpCodes),
391-
URI_ATTRS[attrName.toLowerCase()] ? _sanitizeUrl : null,
391+
SENSITIVE_ATTRS[attrName.toLowerCase()] ? _sanitizeUrl : null,
392392
);
393393
}
394394
}
@@ -816,7 +816,7 @@ function walkIcuTree(
816816
newIndex,
817817
attr.name,
818818
0,
819-
URI_ATTRS[lowerAttrName] ? _sanitizeUrl : null,
819+
SENSITIVE_ATTRS[lowerAttrName] ? _sanitizeUrl : null,
820820
);
821821
} else {
822822
ngDevMode &&
@@ -827,7 +827,7 @@ function walkIcuTree(
827827
);
828828
}
829829
} else if (VALID_ATTRS[lowerAttrName]) {
830-
if (URI_ATTRS[lowerAttrName]) {
830+
if (SENSITIVE_ATTRS[lowerAttrName]) {
831831
// Don't sanitize, because no value is acceptable in sensitive attributes.
832832
// Translators are not allowed to create URIs.
833833
if (typeof ngDevMode !== 'undefined' && ngDevMode) {

packages/core/src/sanitization/html_sanitizer.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,16 @@ import {trustedHTMLFromString} from '../util/security/trusted_types';
1313
import {getInertBodyHelper, InertBodyHelper} from './inert_body';
1414
import {_sanitizeUrl} from './url_sanitizer';
1515

16-
function tagSet(tags: string): {[k: string]: boolean} {
17-
const res: {[k: string]: boolean} = {};
16+
type BooleanRecord = Record<string, boolean>;
17+
18+
function tagSet(tags: string): BooleanRecord {
19+
const res: BooleanRecord = {};
1820
for (const t of tags.split(',')) res[t] = true;
1921
return res;
2022
}
2123

22-
function merge(...sets: {[k: string]: boolean}[]): {[k: string]: boolean} {
23-
const res: {[k: string]: boolean} = {};
24+
function merge(...sets: BooleanRecord[]): BooleanRecord {
25+
const res: BooleanRecord = {};
2426
for (const s of sets) {
2527
for (const v in s) {
2628
if (s.hasOwnProperty(v)) res[v] = true;
@@ -66,15 +68,15 @@ const INLINE_ELEMENTS = merge(
6668
),
6769
);
6870

69-
export const VALID_ELEMENTS: {[k: string]: boolean} = merge(
71+
export const VALID_ELEMENTS: BooleanRecord = merge(
7072
VOID_ELEMENTS,
7173
BLOCK_ELEMENTS,
7274
INLINE_ELEMENTS,
7375
OPTIONAL_END_TAG_ELEMENTS,
7476
);
7577

7678
// Attributes that have href and hence need to be sanitized
77-
export const URI_ATTRS: {[k: string]: boolean} = tagSet(
79+
const URI_ATTRS: BooleanRecord = tagSet(
7880
'background,cite,href,itemtype,longdesc,poster,src,xlink:href',
7981
);
8082

@@ -105,7 +107,7 @@ const ARIA_ATTRS = tagSet(
105107
// can be sanitized, but they increase security surface area without a legitimate use case, so they
106108
// are left out here.
107109

108-
export const VALID_ATTRS: {[k: string]: boolean} = merge(URI_ATTRS, HTML_ATTRS, ARIA_ATTRS);
110+
export const VALID_ATTRS: BooleanRecord = merge(URI_ATTRS, HTML_ATTRS, ARIA_ATTRS);
109111

110112
// Elements whose content should not be traversed/preserved, if the elements themselves are invalid.
111113
//
@@ -114,6 +116,16 @@ export const VALID_ATTRS: {[k: string]: boolean} = merge(URI_ATTRS, HTML_ATTRS,
114116
// don't want to preserve the content, if the elements themselves are going to be removed.
115117
const SKIP_TRAVERSING_CONTENT_IF_INVALID_ELEMENTS = tagSet('script,style,template');
116118

119+
/**
120+
* Attributes that are potential attach vectors and may need to be sanitized.
121+
*/
122+
export const SENSITIVE_ATTRS: BooleanRecord = merge(
123+
URI_ATTRS,
124+
// Note: we don't include these attributes in `URI_ATTRS`, because `URI_ATTRS` also
125+
// determines whether an attribute should be dropped when sanitizing an HTML string.
126+
tagSet('action,formaction,data,codebase'),
127+
);
128+
117129
/**
118130
* SanitizingHtmlSerializer serializes a DOM fragment, stripping out any unsafe elements and unsafe
119131
* attributes.

packages/core/test/acceptance/i18n_spec.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {CommonModule, DOCUMENT, registerLocaleData} from '@angular/common';
1313
import localeEs from '@angular/common/locales/es';
1414
import localeRo from '@angular/common/locales/ro';
1515
import {computeMsgId} from '@angular/compiler';
16+
import {isBrowser} from '@angular/private/testing';
1617
import {
1718
Attribute,
1819
Component,
@@ -3598,6 +3599,27 @@ describe('runtime i18n', () => {
35983599
expect(link).toBeTruthy();
35993600
expect(link.getAttribute('href')).toMatch(/^unsafe:/);
36003601
});
3602+
3603+
it('should sanitize action binding', () => {
3604+
const fixture = initWithTemplate(
3605+
SanitizeAppComp,
3606+
'<form action="{{url}}" i18n-action></form>',
3607+
);
3608+
const form: HTMLFormElement = fixture.nativeElement.querySelector('form');
3609+
expect(form.getAttribute('action')).toMatch(/^unsafe:/);
3610+
});
3611+
3612+
// Skip this test in Node, because Domino doesn't support `formAction`.
3613+
if (isBrowser) {
3614+
it('should sanitize formaction binding', () => {
3615+
const fixture = initWithTemplate(
3616+
SanitizeAppComp,
3617+
'<input type="text" formaction="{{url}}" i18n-formaction>',
3618+
);
3619+
const input: HTMLInputElement = fixture.nativeElement.querySelector('input');
3620+
expect(input.getAttribute('formaction')).toMatch(/^unsafe:/);
3621+
});
3622+
}
36013623
});
36023624
});
36033625

packages/core/test/acceptance/security_spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -752,3 +752,19 @@ describe('SVG animation processing', () => {
752752
);
753753
});
754754
});
755+
756+
describe('innerHTML processing', () => {
757+
it('should drop risky attributes from elements created with innerHTML', () => {
758+
@Component({
759+
template: '<div [innerHTML]="html"></div>',
760+
})
761+
class App {
762+
html = '<div action="abc"></div>';
763+
}
764+
765+
const fixture = TestBed.createComponent(App);
766+
fixture.detectChanges();
767+
768+
expect(fixture.nativeElement.innerHTML).not.toContain('action');
769+
});
770+
});

0 commit comments

Comments
 (0)