Managing External Links Safely in Angular
Cory Rylan
- 4 minutes
UpdatedWhen building large enterprise Angular applications, there are times where the user may
need to leave the application temporarily to complete some task. Often when
we provide a link to a separate domain, we can apply a particular attribute
target="_blank"
to open that link in a new tab. Opening a link in a new tab
can be useful to keep the application running and not lose work in progress.
<a href="https://example.com" target="_blank">
Example External Link
</a>
While opening a link in a new tab can be convenient at times it can pose a security
and performance risk. When using target="_blank"
the referring domain runs on the
same process as your application which causes two issues. First, by being on the
same process, the referring domain can access details about your application
via the window.opener
property and bypasses the default isolated behavior of
browser tabs. The other issue is a performance problem as now your application, and the
referring link is running on the same process and has to share resources.
To solve both of these issues any link using target="_blank"
should also have
a rel="noopener"
attribute as well. The rel="noopener"
attribute makes
sure that the new browser tab does not run on the same process and prevent it from accessing
window.opener
.
<a href="https://example.com" target="_blank" rel="noopener">
Example External Link
</a>
This attribute fixes our performance and security issue, but now we must manually check every link in our application to make sure if it is external that it has the appropriate attributes. Luckily for us, we can leverage Angular Directives to automate this for us.
Angular Directive
Angular Directives typically are attribute-based selectors. With Directives however, you can use any valid CSS selector.
import { Directive } from '@angular/core';
@Directive({
selector: 'a[href]'
})
export class ExternalLinkDirective {}
In the example Directive, we can select all anchor tags with an href
attribute.
This selector allows us to inspect all links in our application and determine
how we should update them.
import { Directive, HostBinding, Input } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
@Directive({
selector: 'a[href]'
})
export class ExternalLinkDirective {
@HostBinding('attr.rel') relAttr = '';
@HostBinding('attr.target') targetAttr = '';
@HostBinding('attr.href') hrefAttr = '';
@Input() href: string;
ngOnChanges() {
this.hrefAttr = this.href;
if (this.isLinkExternal()) {
this.relAttr = 'noopener';
this.targetAttr = '_blank';
}
}
private isLinkExternal() {
return !this.href.includes(location.hostname);
}
}
Using the HostBinding
decorator, we can access the attributes of the host element
which is the link the directive is attached. In this use case, we access the rel
,
target
abd href
attributes.
Using the @Input
decorator we can get the value of the href
on the link. Because
we set the href
as an input then whenever the href
changes the ngOnChanges
life cycle will be called. We need to hook into ngOnChanges
that way if the link
id dynamically created our directive knows to check the href
value and update.
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
template: `<a href="https://{{dynamic}}">Dynamic Link</a>`
})
export class AppComponent {
dynamic = 'example.com';
}
When taking in the href as an @Input
Angular stops reflecting or updating the
DOM href
attribute on our links. Because of this we have to use a @HostBinding
on the href
attribute to reassign href
attribute so its updated in the DOM.
export class ExternalLinkDirective {
...
@HostBinding('attr.href') hrefAttr = '';
@Input() href: string;
ngOnChanges() {
this.hrefAttr = this.href;
...
}
}
Now anytime the input changes we update the href
attribute in the DOM as well.
In the ngOnChanges
we check if the link is external or not by checking the current
domain using the native browser API location.hostname
.
export class ExternalLinkDirective {
@HostBinding('attr.rel') relAttr = '';
@HostBinding('attr.target') targetAttr = '';
@HostBinding('attr.href') hrefAttr = '';
@Input() href: string;
ngOnChanges() {
this.hrefAttr = this.href;
if (this.isLinkExternal()) {
this.relAttr = 'noopener';
this.targetAttr = '_blank';
}
}
private isLinkExternal() {
return !this.href.includes(location.hostname);
}
}
If the link is external to our application then we set the rel
and target
attributes
appropriately. Now all links in our application are external when clicked open into
a separate tab safely. There is a little more work to make this code safe
for server-side rendering with Angular Universal.
import {
Directive,
HostBinding,
PLATFORM_ID,
Inject,
Input
} from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
@Directive({
selector: 'a[href]'
})
export class ExternalLinkDirective {
@HostBinding('attr.rel') relAttr = '';
@HostBinding('attr.target') targetAttr = '';
@HostBinding('attr.href') hrefAttr = '';
@Input() href: string;
constructor(@Inject(PLATFORM_ID) private platformId: string) {}
ngOnChanges() {
this.hrefAttr = this.href;
if (this.isLinkExternal()) {
this.relAttr = 'noopener';
this.targetAttr = '_blank';
}
}
private isLinkExternal() {
return (
isPlatformBrowser(this.platformId) &&
!this.href.includes(location.hostname)
);
}
}
If this is running in a server-side application, we need to check that way we do
not try to access the browser only APIs like location
. To check if we are running
on the server, we inject the platformId
. This gives us a token value that we
can pass to the isPlatformBrowser()
function. Now our code only runs on the client
and prevents and errors on the server.
To see the full working example check out the demo link below. To get more information
about the security and performance risks of using target="_blank"
check out
the Google Developer Documentation.