Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ template: function MyComponent_Template(rf, ctx) {
$r3$.ɵɵadvance();
$r3$.ɵɵattribute("style", ctx.evil, $r3$.ɵɵsanitizeStyle);
$r3$.ɵɵadvance();
$r3$.ɵɵdomProperty("src", ctx.nonEvil, $r3$.ɵɵsanitizeUrl);
$r3$.ɵɵdomProperty("src", ctx.nonEvil);
$r3$.ɵɵadvance();
$r3$.ɵɵdomProperty("sandbox", ctx.evil, $r3$.ɵɵvalidateAttribute);
$r3$.ɵɵadvance();
Expand All @@ -23,7 +23,7 @@ template: function MyComponent_Template(rf, ctx) {
$r3$.ɵɵadvance();
$r3$.ɵɵtwoWayProperty("srcdoc", ctx.evil, $r3$.ɵɵsanitizeHtml);
$r3$.ɵɵadvance();
$r3$.ɵɵtwoWayProperty("src", ctx.evil, $r3$.ɵɵsanitizeUrl);
$r3$.ɵɵtwoWayProperty("src", ctx.evil);
$r3$.ɵɵadvance();
$r3$.ɵɵtwoWayProperty("src", ctx.evil, $r3$.ɵɵsanitizeResourceUrl);
$r3$.ɵɵadvance();
Expand Down
4 changes: 0 additions & 4 deletions packages/compiler/src/schema/dom_security_schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,6 @@ export function SECURITY_SCHEMA(): SecuritySchema {
['area', ['href']],
['a', ['href', 'xlink:href']],
['form', ['action']],

// The below two items are safe and should be removed but they require a G3 clean-up as a small number of tests fail.
['img', ['src']],
['video', ['src']],
]);

registerContext(SecurityContext.URL, MATH_ML_NAMESPACE, [
Expand Down
34 changes: 34 additions & 0 deletions packages/core/src/render3/instructions/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {hasSkipHydrationAttrOnRElement} from '../../hydration/skip_hydration';
import {PRESERVE_HOST_CONTENT, PRESERVE_HOST_CONTENT_DEFAULT} from '../../hydration/tokens';
import {processTextNodeMarkersBeforeHydration} from '../../hydration/utils';
import {ViewEncapsulation} from '../../metadata/view';
import {unwrapSafeValue} from '../../sanitization/bypass';
import {validateAgainstEventProperties} from '../../sanitization/sanitization';

import {ProfilerEvent} from '../../../primitives/devtools';
Expand Down Expand Up @@ -305,6 +306,15 @@ export function setDomProperty<T>(
// It is assumed that the sanitizer is only added when the compiler determines that the
// property is risky, so sanitization can be done without further checks.
value = sanitizer != null ? (sanitizer(value, tNode.value || '', propName) as any) : value;

// The `src` property previously used a sanitizer which mapped `null`/`undefined` to `''`.
// Now that `img` and `video` `src` are no longer sanitized, we still need to map `null` and
// `undefined` to `''` to avoid stringifying them to `'null'` or `'undefined'` and causing
// broken network requests.

// TODO(v23): Remove this workaround once we can introduce a breaking change
value = unwrapImgVideoSrcValue(propName, element as RElement, value);

renderer.setProperty(element as RElement, propName, value);
} else if (tNode.type & TNodeType.AnyContainer) {
// If the node is a container and the property didn't
Expand All @@ -315,6 +325,28 @@ export function setDomProperty<T>(
}
}

/**
* This function allows us to workaround a breaking change introduced by #71095
* src attributes/bindings used to be sanitized which was responsible for:
* - converting undefined/null to ''
* - supporting bypassed values (via bypassSecurityTrustResourceUrl)
*
* This workaround is intended to be dropped in v23 when the breaking change window opens.
*/
function unwrapImgVideoSrcValue(propName: string, element: RElement, value: unknown): any {
if (
propName === 'src' &&
((element as RElement).tagName === 'IMG' || (element as RElement).tagName === 'VIDEO')
) {
if (value == null) {
return '' as any;
}

return unwrapSafeValue(value);
}
return value;
}

/** If node is an OnPush component, marks its LView dirty. */
export function markDirtyIfOnPush(lView: LView, viewIndex: number): void {
ngDevMode && assertLView(lView);
Expand Down Expand Up @@ -515,6 +547,8 @@ export function elementAttributeInternal(
}

const element = getNativeByTNode(tNode, lView) as RElement;
// TODO(v23): Remove this workaround once we can introduce a breaking change
value = unwrapImgVideoSrcValue(name, element, value);
setElementAttribute(lView[RENDERER], element, namespace, tNode.value, name, value, sanitizer);
}

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/sanitization/bypass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ abstract class SafeValueImpl implements SafeValue {
toString() {
return (
`SafeValue must use [property]=binding: ${this.changingThisBreaksApplicationSecurity}` +
` (see ${XSS_SECURITY_URL})`
(ngDevMode ? ` (see ${XSS_SECURITY_URL})` : '')
);
}
}
Expand Down
4 changes: 0 additions & 4 deletions packages/core/src/sanitization/dom_security_schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,6 @@ export function SECURITY_SCHEMA(): SecuritySchema {
['area', ['href']],
['a', ['href', 'xlink:href']],
['form', ['action']],

// The below two items are safe and should be removed but they require a G3 clean-up as a small number of tests fail.
['img', ['src']],
['video', ['src']],
]);

registerContext(SecurityContext.URL, MATH_ML_NAMESPACE, [
Expand Down
22 changes: 21 additions & 1 deletion packages/core/test/acceptance/attributes_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@
*/

import {By, DomSanitizer, SafeUrl} from '@angular/platform-browser';
import {ChangeDetectionStrategy, Component, provideZoneChangeDetection} from '../../src/core';
import {
ChangeDetectionStrategy,
Component,
inject,
provideZoneChangeDetection,
} from '../../src/core';
import {TestBed} from '../../testing';

describe('attribute creation', () => {
Expand Down Expand Up @@ -209,6 +214,21 @@ describe('attribute binding', () => {
// should not start with `unsafe:`.
expect(a.href.indexOf('unsafe:')).toBe(-1);
});

it('should bind attribute src and not expose a safe value', () => {
@Component({
template: `<img [attr.src]="badUrl" />`,
})
class Comp {
badUrl = inject(DomSanitizer).bypassSecurityTrustUrl('javascript:true');
}

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

const img = fixture.debugElement.query(By.css('img')).nativeElement;
expect(img.getAttribute('src')).toBe('javascript:true');
});
});

describe('attribute interpolation', () => {
Expand Down
26 changes: 26 additions & 0 deletions packages/core/test/acceptance/property_binding_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,32 @@ describe('property bindings', () => {
expect(spanEl.id).toBe('testId');
});

it('should not set `src` to "undefined" string for img/video elements when bound to undefined', () => {
@Component({
template: `
<img [src]="imgSrc" />
<video [src]="videoSrc"></video>
`,
})
class Comp {
imgSrc: string | undefined;
videoSrc: string | undefined;
}

const fixture = TestBed.createComponent(Comp);
fixture.detectChanges();
Comment thread
JeanMeche marked this conversation as resolved.

const img = fixture.nativeElement.querySelector('img') as HTMLImageElement;
const video = fixture.nativeElement.querySelector('video') as HTMLVideoElement;

// Both should have their src attribute set to an empty string, not "undefined"
// (Wait, actually they might not have the attribute if the property is set to '', but `img.src` should not contain 'undefined')
expect(img.getAttribute('src')).not.toBe('undefined');
expect(img.getAttribute('src')).toBe('');
expect(video.getAttribute('src')).not.toBe('undefined');
expect(video.getAttribute('src')).toBe('');
});

it('should update bindings when value changes', () => {
@Component({
template: `<a [title]="title"></a>`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@
"SUBSTITUTION_EXPR_START",
"SVG_NAMESPACE",
"SafeSubscriber",
"SafeValueImpl",
"Sanitizer",
"ShadowDomRenderer",
"SharedStylesHost",
Expand Down Expand Up @@ -849,7 +850,9 @@
"unregisterLView",
"unregisteredTrigger",
"unsupportedTriggerEvent",
"unwrapImgVideoSrcValue",
"unwrapRNode",
"unwrapSafeValue",
"updateAncestorTraversalFlagsOnAttach",
"updateMicroTaskStatus",
"validateStyleParams",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,6 @@
"ViewContext",
"ViewEncapsulation",
"ViewRef",
"XSS_SECURITY_URL",
"ZONELESS_ENABLED",
"ZoneAwareEffectScheduler",
"\\u0275FORM_CONTROL_INTEGRATION",
Expand Down Expand Up @@ -1055,6 +1054,7 @@
"unregisterLView",
"untracked",
"untracked2",
"unwrapImgVideoSrcValue",
"unwrapRNode",
"unwrapResponse",
"unwrapSafeValue",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,6 @@
"ViewContext",
"ViewEncapsulation",
"ViewRef",
"XSS_SECURITY_URL",
"ZONELESS_ENABLED",
"ZoneAwareEffectScheduler",
"\\u0275FORM_CONTROL_INTEGRATION",
Expand Down Expand Up @@ -1053,6 +1052,7 @@
"unregisterLView",
"untracked",
"untracked2",
"unwrapImgVideoSrcValue",
"unwrapRNode",
"unwrapResponse",
"unwrapSafeValue",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1169,6 +1169,7 @@
"untracked",
"untracked2",
"unwrapElementRef",
"unwrapImgVideoSrcValue",
"unwrapRNode",
"unwrapSafeValue",
"updateAncestorTraversalFlagsOnAttach",
Expand Down
Loading