Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/compiler/src/schema/trusted_types_sinks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* tags use '*'.
*
* Extracted from, and should be kept in sync with
* https://w3c.github.io/webappsec-trusted-types/dist/spec/#integrations
* https://www.w3.org/TR/trusted-types/#integrations
*/
const TRUSTED_TYPES_SINKS = new Set<string>([
// NOTE: All strings in this set *must* be lowercase!
Expand All @@ -25,6 +25,7 @@ const TRUSTED_TYPES_SINKS = new Set<string>([

// TrustedScriptURL
'embed|src',
'iframe|src',
'object|codebase',
'object|data',
]);
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/test/schema/trusted_types_sinks_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ describe('isTrustedTypesSink', () => {
expect(isTrustedTypesSink('iframe', 'srcdoc')).toBeTrue();
expect(isTrustedTypesSink('p', 'innerHTML')).toBeTrue();
expect(isTrustedTypesSink('embed', 'src')).toBeTrue();
expect(isTrustedTypesSink('iframe', 'src')).toBeTrue();
expect(isTrustedTypesSink('a', 'href')).toBeFalse();
expect(isTrustedTypesSink('base', 'href')).toBeFalse();
expect(isTrustedTypesSink('div', 'style')).toBeFalse();
Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/render3/i18n/i18n_parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import '../../util/ng_i18n_closure_mode';
import {XSS_SECURITY_URL} from '../../error_details_base_url';
import {
getTemplateContent,
URI_ATTRS,
SENSITIVE_ATTRS,
VALID_ATTRS,
VALID_ELEMENTS,
} from '../../sanitization/html_sanitizer';
Expand Down Expand Up @@ -388,7 +388,7 @@ export function i18nAttributesFirstPass(tView: TView, index: number, values: str
previousElementIndex,
attrName,
countBindings(updateOpCodes),
URI_ATTRS[attrName.toLowerCase()] ? _sanitizeUrl : null,
SENSITIVE_ATTRS[attrName.toLowerCase()] ? _sanitizeUrl : null,
);
}
}
Expand Down Expand Up @@ -816,7 +816,7 @@ function walkIcuTree(
newIndex,
attr.name,
0,
URI_ATTRS[lowerAttrName] ? _sanitizeUrl : null,
SENSITIVE_ATTRS[lowerAttrName] ? _sanitizeUrl : null,
);
} else {
ngDevMode &&
Expand All @@ -827,7 +827,7 @@ function walkIcuTree(
);
}
} else if (VALID_ATTRS[lowerAttrName]) {
if (URI_ATTRS[lowerAttrName]) {
if (SENSITIVE_ATTRS[lowerAttrName]) {
// Don't sanitize, because no value is acceptable in sensitive attributes.
// Translators are not allowed to create URIs.
if (typeof ngDevMode !== 'undefined' && ngDevMode) {
Expand Down
26 changes: 19 additions & 7 deletions packages/core/src/sanitization/html_sanitizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ import {trustedHTMLFromString} from '../util/security/trusted_types';
import {getInertBodyHelper, InertBodyHelper} from './inert_body';
import {_sanitizeUrl} from './url_sanitizer';

function tagSet(tags: string): {[k: string]: boolean} {
const res: {[k: string]: boolean} = {};
type BooleanRecord = Record<string, boolean>;

function tagSet(tags: string): BooleanRecord {
const res: BooleanRecord = {};
for (const t of tags.split(',')) res[t] = true;
return res;
}

function merge(...sets: {[k: string]: boolean}[]): {[k: string]: boolean} {
const res: {[k: string]: boolean} = {};
function merge(...sets: BooleanRecord[]): BooleanRecord {
const res: BooleanRecord = {};
for (const s of sets) {
for (const v in s) {
if (s.hasOwnProperty(v)) res[v] = true;
Expand Down Expand Up @@ -66,15 +68,15 @@ const INLINE_ELEMENTS = merge(
),
);

export const VALID_ELEMENTS: {[k: string]: boolean} = merge(
export const VALID_ELEMENTS: BooleanRecord = merge(
VOID_ELEMENTS,
BLOCK_ELEMENTS,
INLINE_ELEMENTS,
OPTIONAL_END_TAG_ELEMENTS,
);

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

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

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

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

/**
* Attributes that are potential attach vectors and may need to be sanitized.
*/
export const SENSITIVE_ATTRS: BooleanRecord = merge(
URI_ATTRS,
// Note: we don't include these attributes in `URI_ATTRS`, because `URI_ATTRS` also
// determines whether an attribute should be dropped when sanitizing an HTML string.
tagSet('action,formaction,data,codebase'),
);

/**
* SanitizingHtmlSerializer serializes a DOM fragment, stripping out any unsafe elements and unsafe
* attributes.
Expand Down
22 changes: 22 additions & 0 deletions packages/core/test/acceptance/i18n_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {CommonModule, DOCUMENT, registerLocaleData} from '@angular/common';
import localeEs from '@angular/common/locales/es';
import localeRo from '@angular/common/locales/ro';
import {computeMsgId} from '@angular/compiler';
import {isBrowser} from '@angular/private/testing';
import {
Attribute,
Component,
Expand Down Expand Up @@ -3598,6 +3599,27 @@ describe('runtime i18n', () => {
expect(link).toBeTruthy();
expect(link.getAttribute('href')).toMatch(/^unsafe:/);
});

it('should sanitize action binding', () => {
const fixture = initWithTemplate(
SanitizeAppComp,
'<form action="{{url}}" i18n-action></form>',
);
const form: HTMLFormElement = fixture.nativeElement.querySelector('form');
expect(form.getAttribute('action')).toMatch(/^unsafe:/);
});

// Skip this test in Node, because Domino doesn't support `formAction`.
if (isBrowser) {
it('should sanitize formaction binding', () => {
const fixture = initWithTemplate(
SanitizeAppComp,
'<input type="text" formaction="{{url}}" i18n-formaction>',
);
const input: HTMLInputElement = fixture.nativeElement.querySelector('input');
expect(input.getAttribute('formaction')).toMatch(/^unsafe:/);
});
}
});
});

Expand Down
16 changes: 16 additions & 0 deletions packages/core/test/acceptance/security_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -752,3 +752,19 @@ describe('SVG animation processing', () => {
);
});
});

describe('innerHTML processing', () => {
it('should drop risky attributes from elements created with innerHTML', () => {
@Component({
template: '<div [innerHTML]="html"></div>',
})
class App {
html = '<div action="abc"></div>';
}

const fixture = TestBed.createComponent(App);
fixture.detectChanges();

expect(fixture.nativeElement.innerHTML).not.toContain('action');
});
});
Loading