<h1>
<small>Reproduction of AngularJS vulnerability:</small><br />
No sanitization for <code>source[srcset]</code>
</h1>
<section class="vulnerability-info-section">
<h2>Vulnerability information</h2>
<table class="vulnerability-info-table">
<tr>
<th>CVE:</th>
<td><a href="https://www.cve.org/CVERecord?id=CVE-2024-8373">CVE-2024-8373</a></td>
</tr>
<tr>
<th>Type:</th>
<td>Image source sanitization bypass (a form of <a href="https://owasp.org/www-community/attacks/Content_Spoofing">Content Spoofing</a>)</td>
</tr>
<tr>
<th>Affected versions:</th>
<td>>=0.0.0</td>
</tr>
<tr>
<th>Fixed in version:</th>
<td><a href="https://www.herodevs.com/support/nes-angularjs">AngularJS NES</a> v1.9.6 and v1.5.22</td>
</tr>
<tr>
<th>Description:</th>
<td>
Setting a <code><source></code> element's <code>srcset</code> attribute value via the <a href="https://docs.angularjs.org/guide/interpolation#-ngattr-for-binding-to-arbitrary-attributes">ngAttrSrcset</a> directive or <a href="https://docs.angularjs.org/guide/interpolation">interpolation</a> is not subject to <a href="https://docs.angularjs.org/api/ng/provider/$compileProvider#imgSrcSanitizationTrustedUrlList">image source sanitization</a>. As a result, no restrictions are applied to the images that can be shown, which can also lead to a form of <a href="https://owasp.org/www-community/attacks/Content_Spoofing">Content Spoofing</a>.
<aside class="info">
<b>NOTE</b><br />
The <a href="https://docs.angularjs.org/api/ng/directive/ngSrcset">ngSrcset</a> and <a href="https://docs.angularjs.org/api/ng/directive/ngProp">ngPropSrcset</a> directives are not affected. With these directives, sanitization works as intended.
</aside>
</td>
</tr>
</table>
</section>
<section class="repro-instructions-section">
<h2>Reproduction instructions</h2>
<p>
The app below demonstrates the issue by trying to load images that should be blocked by the image source sanitization rules. It is doing so by binding to a <code><source></code> element's <code>srcset</code> attribute using <code>ngSrcset</code>, <code>ngAttrSrcset</code>, <code>ngPropSrcset</code> and <code>srcset</code> interpolation.
</p>
<p>
For demonstration purposes, the app is configured to only allow images from the <code>https://angularjs.org/</code> domain:
<pre>
$compileProvider.imgSrcSanitizationTrustedUrlList(/^https:\/\/angularjs\.org\//);
</pre>
</p>
<p>
However, by taking advantage of the lack of sanitization in <code>ngAttrSrcset</code> and <code>srcset</code> interpolation, we are able to show images from other domains (e.g. <code>https://angular.dev/</code>) as well as different schemes (e.g. an arbitrary SVG image using the <code>data:image/svg+xml</code> format).
</p>
<p>
<b>Steps:</b>
<ol>
<li>
Take a look at the table below.
</li>
<li>
For reference, the first 4 entries use the <code>ngSrcset</code> and <code>ngPropSrcset</code> directives (which are not vulnerable) and demonstrate that the image sources are sanitized correctly, not allowing images outside <code>https://angularjs.org/</code> to show.
</li>
<li>
In contrast, the last 4 entries use the <code>ngAttrSrcset</code> directive or interpolation with the <code>srcset</code> attribute, which are not subject to sanitization, thus allowing us to show images that should be blocked.
</li>
</ol>
</p>
</section>
<section class="repro-content-section" ng-app="app">
<h2>Reproduction</h2>
<p class="repro-images">
<span class="section-header">
Examples with <code>ngSrcset</code><br />
(not vulnerable)
</span>
<span>
Image from <code>angular.dev</code>:<br />
<b>BLOCKED</b> (Correct ✔️)
</span>
<picture>
<source ng-srcset="https://angular.dev/favicon.ico" />
<img src="" />
</picture>
<span>
Arbitrary SVG image via data URL:<br />
<b>BLOCKED</b> (Correct ✔️)
</span>
<picture>
<source ng-srcset="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48dGV4dCBmb250LXNpemU9IjEwMCIgeT0iMWVtIj7wn5Cx4oCN8J+SuzwvdGV4dD48L3N2Zz4=" />
<img src="" />
</picture>
<span class="section-header">
Examples with <code>ngPropSrcset</code><br />
(not vulnerable)
</span>
<span>
Image from <code>angular.dev</code>:<br />
<b>BLOCKED</b> (Correct ✔️)
</span>
<picture>
<source ng-prop-srcset="'https://angular.dev/favicon.ico'" />
<img src="" />
</picture>
<span>
Arbitrary SVG image via data URL:<br />
<b>BLOCKED</b> (Correct ✔️)
</span>
<picture>
<source ng-prop-srcset="'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48dGV4dCBmb250LXNpemU9IjEwMCIgeT0iMWVtIj7wn5Cx4oCN8J+SuzwvdGV4dD48L3N2Zz4='" />
<img src="" />
</picture>
<span class="section-header">
Examples with <code>ngAttrSrcset</code><br />
(vulnerable)
</span>
<span>
Image from <code>angular.dev</code>:<br />
<b>ALLOWED</b> (Error ❌)
</span>
<picture>
<source ng-attr-srcset="https://angular.dev/favicon.ico" />
<img src="" />
</picture>
<span>
Arbitrary SVG image via data URL:<br />
<b>ALLOWED</b> (Error ❌)
</span>
<picture>
<source ng-attr-srcset="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48dGV4dCBmb250LXNpemU9IjEwMCIgeT0iMWVtIj7wn5Cx4oCN8J+SuzwvdGV4dD48L3N2Zz4=" />
<img src="" />
</picture>
<span class="section-header">
Examples with <code>srcset</code> interpolation<br />
(vulnerable)
</span>
<span>
Image from <code>angular.dev</code>:<br />
<b>ALLOWED</b> (Error ❌)
</span>
<picture>
<source srcset="{{ 'https://angular.dev/favicon.ico' }}" />
<img src="" />
</picture>
<span>
Arbitrary SVG image via data URL:<br />
<b>ALLOWED</b> (Error ❌)
</span>
<picture>
<source srcset="{{ 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48dGV4dCBmb250LXNpemU9IjEwMCIgeT0iMWVtIj7wn5Cx4oCN8J+SuzwvdGV4dD48L3N2Zz4=' }}" />
<img src="" />
</picture>
</p>
</section>
/* Styles */
aside {
border: 0.1rem solid lightgray;
border-radius: 0.5rem;
font-size: 0.9rem;
margin: 0.5rem;
padding: 0.5rem;
}
aside.info {
background-color: aliceblue;
}
aside.warn {
background-color: lemonchiffon;
}
body {
font-family: sans-serif;
font-size: 1rem;
}
code {
font-size: 1.1em;
}
code::before,
code::after {
content: '`';
}
h1 {
text-align: center;
}
h1 > small {
display: inline-block;
font-style: italic;
padding-block-end: 0.5rem;
}
pre {
background-color: lightgray;
border: 1px solid dimgray;
border-radius: 0.25rem;
overflow: auto;
padding: 0.5rem;
white-space: pre-line;
}
section {
border-block-start: 2px solid gray;
padding: 0 1rem 1rem;
}
.vulnerability-info-table {
border-spacing: 0.5rem;
margin-inline-end: 1rem;
}
.vulnerability-info-table th {
text-align: start;
vertical-align: top;
white-space: nowrap;
}
.repro-content-section button {
margin: 0.25rem;
}
.repro-content-section .repro-btn {
position: relative;
}
.repro-content-section .repro-btn::after {
animation: spinner 0.5s linear infinite;
border-radius: 50%;
border-right: 2px solid transparent;
border-top: 2px solid darkorchid;
content: '';
display: none;
height: 1rem;
margin-top: -0.5rem;
position: absolute;
right: -2rem;
top: 50%;
width: 1rem;
}
.repro-content-section .repro-btn:active::after {
display: initial;
}
.repro-content-section .repro-btn:active + .repro-duration {
visibility: hidden;
}
.repro-content-section .repro-duration {
border-inline-start: 2px solid gray;
padding-inline-start: 0.33rem;
}
.repro-content-section .repro-images {
display: grid;
gap: 1rem;
grid-template-columns: auto 100px;
grid-auto-rows: 1fr;
justify-content: start;
place-items: center stretch;
}
.repro-content-section .repro-images img {
justify-self: center;
max-width: 50px;
}
.repro-content-section .repro-images source[srcset^="unsafe\:"]:after {
content: '🚫 BLOCKED';
white-space: pre;
}
.repro-content-section .repro-images .section-header {
border-top: 1px solid;
font-weight: bold;
grid-column: 1 / -1;
padding-top: 1rem;
text-align: center;
text-decoration: underline;
}
.repro-instructions-section li {
margin-block-end: 0.5rem;
}
/* Animation keyframes */
@keyframes spinner {
to {
transform: rotate(360deg);
}
}
// Define and configure the app.
angular
.module('app', [])
.config(['$compileProvider', $compileProvider => {
$compileProvider.imgSrcSanitizationTrustedUrlList(
// Only allow images from `angularjs.org`.
/^https:\/\/angularjs\.org\//);
}]);
This Pen doesn't use any external CSS resources.