Loading images in a web application is something every developer handles routinely. The typical approach involves an img element with a src attribute, and the browser takes care of the rest. However, this straightforward method carries a significant security flaw.
Consider a multi-user application designed for managing pictures. Each user naturally has exclusive access to their own images, and user A should never be able to view user B's content. Adding authorization to this scenario is tricky because the image is fetched by the browser directly through the DOM. Look at this Angular example:
@Component({
template: `
<img [src]="img.src"/>
`
})
export class FooComponent {
img = {
src: 'https://angular.io/assets/images/logos/angular/logo-nav@2x.png'
}
}
The browser sends a standard HTTP request to load the resource. Without extra information, how can the backend distinguish between a request from user A and one from user B? The server must receive some form of authorization before it can respond appropriately.
Using session cookies for authorization
One way to handle this is with session cookies. The process works like this: a user logs in, the backend issues a session cookie, and that cookie accompanies every subsequent request. This way, the server always knows the identity of the requester.
Session cookies are not universally loved; many prefer stateless backends because they scale better. With modern authentication schemes like JWT, the standard practice is to include an authorization-header rather than relying on cookies. The value in that header is called a Bearer token.
Passing the authorization without cookies
Here are two distinct techniques for sending Bearer tokens along with image requests:
Passing the token in the url
You can put the token into the url as a query parameter. Our earlier snippet changes to:
@Component({
template: `
<img [src]="img.src + '?bearer=' + bearToken"/>
`
})
export class FooComponent {
img = {
src: 'https://angular.io/assets/images/logos/angular/logo-nav@2x.png'
}
bearerToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';
}
The request url becomes: https://angular.io/assets/images/logos/angular/logo-nav@2x.png?bearer=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…
Using this token, the backend identifies who is trying to retrieve the image and can reject unauthorized attempts.
However, this approach has its drawbacks:
- The token is visible in the url to anyone who looks (even if they might not have access to the offline storage)
- A new token changes the url, which invalidates any cached version of the image
- The backend must be set up to parse bearer tokens from image request urls
- We need to construct the token and append it to the url every time we display an image in a component
- This method doesn't apply to css files unless generated dynamically
Handling the images with AJAX calls
In this method, we build a generic mechanism for secure images using several pieces:
- A reusable component
- AJAX calls with a blob responseType
- HTTP interceptors (which came with the new HttpClient in angular 4.3)
- Data urls
Imagine consuming images like this:
@Component({
template: `
<secured-image [src]="img.src"></secured-image>
`
})
export class FooComponent {
img = {
src: 'https://angular.io/assets/images/logos/angular/logo-nav@2x.png'
}
}
Let's put together a secured-image component like the one shown above. This component has to take care of the following requirements:
- Fetch the image via an AJAX call
- Transform a blob into a data url
- React to changes in the source:
- Abort the previous AJAX operation (provided it's still active)
- Initiate a new request for the updated resource
- When the component is destroyed, terminate any active AJAX operation
The initial version of the component could be written like this:
@Component({
selector: 'secured-image',
template: `
<img [src]="dataUrl$|async"/>
`
})
export class SecuredImageComponent implements OnChanges {
// This code block just creates an rxjs stream from the src
// this makes sure that we can handle source changes
// or even when the component gets destroyed
// So basically turn src into src$
@Input() private src: string;
private src$ = new BehaviorSubject(this.src);
ngOnChanges(): void {
this.src$.next(this.src);
}
// this stream will contain the actual url that our img tag will load
// everytime the src changes, the previous call would be canceled and the
// new resource would be loaded
dataUrl$ = this.src$.switchMap(url => this.loadImage(url))
// we need HttpClient to load the image
constructor(private httpClient: HttpClient) {
}
private loadImage(url: string): Observable<any> {
return this.httpClient
// load the image as a blob
.get(url, {responseType: 'blob'})
// create an object url of that blob that we can use in the src attribute
.map(e => URL.createObjectURL(e))
}
}
This handles all the main points, but testing it in a browser reveals a warning message: WARNING: sanitizing unsafe URL value blob:https://localhost:4200/da89c71e-5df2-4842-af06-993cd5263471 (see http://g.co/ng/security#xss)
The AJAX approach fails without a crucial step: we must sanitize the resulting url. Angular's DomSanitizer is the tool for this task. It's a security measure aimed at preventing XSS. We need to explicitly mark which urls are safe for Angular to trust.
export class SecuredImageComponent implements OnChanges {
...
// inject the domSanitizer here as well
constructor(private httpClient: HttpClient, private domSanitizer: DomSanitizer) {
}
private loadImage(url: string): Observable<any> {
return this.httpClient
.get(...)
// pass the url through the domSanitizer so angular knows he can parse it
.map(e => this.domSanitizer.bypassSecurityTrustUrl(URL.createObjectURL(e)))
}
}
Now we have a robust method for loading images through AJAX. But the Bearer token still hasn't been attached. We could add the header directly in the get request, but a more elegant alternative exists. The new HttpClient, available since angular 4.3, introduces a powerful feature: interceptors. They allow us to hook into any HTTP call made through HttpClient.
This seems like the ideal place to inject the bearer token, right?
Let's create an interceptor and register it with Angular:
// my-http.interceptor.ts
@Injectable()
export class MyHttpInterceptor implements HttpInterceptor {
// intercept any http call done by the httpClient
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// fetch the bearer token from wherever you have stored it
// NOTE: fetching it directly from window is not a good idea (demo purpose)
const jwtToken = window.localStorage.getItem('jwtToken');
// if there is a token, clone the request and set the correct
// authorization header, if not => just use the old request
const requestToHandle = jwtToken
? request.clone({
headers: request.headers.set('authorization', `Bearer ${jwtToken}`)
})
: request;
return next.handle(requestToHandle);
}
}
// app.module.ts
@NgModule({
...
// don't forget to import the HttpClientModule
imports: [ BrowserModule, FormsModule, HttpClientModule ],
providers: [{
// register the interceptor to our angular module
provide: HTTP_INTERCEPTORS, useClass: MyHttpInterceptor, multi: true
}]
})
export class AppModule { }
For more details on interceptors, check out this excellent article by Juri Strumpflohner. He also has an outstanding egghead course on the topic.
With this setup, every request coming from the secured-image component includes the correct authorization header. The server can now verify the requester before serving the image.
This solution does come with its own set of challenges:
- CORS headers for CDN resources. The AJAX GET request triggers additional OPTIONS preflight calls.
Additional benefits:
- We can manage a loading state, showing a spinner or other indicators
- Doesn't apply to css files unless dynamically generated
A complete working version is available in this stackblitz example.
Thanks for reading
I hope you enjoyed the article, and feel free to ask any questions!

•