A Cross-Site Script Inclusion (XSSI) attack occurs when a malicious site imports JavaScript from a third-party domain and is able to extract sensitive details like user credentials from the imported script.
Risks
If your website stores sensitive data in JavaScript files, an attacker will be able to trick your users into visiting a malicious site which imports your JavaScript code, allowing the attacker to scoop up any sensitive data included within that code.
Anatomy of an XSSI Attack
JavaScript files are not subject to the same-origin policy in browsers in the same way that other types of content (like JSON and HTML) are. This allows JavaScript files to be included from different domains when a webpage is rendered, but poses a unique opportunity for attackers to steal any sensitive data written in a JavaScript file.
JavaScript is frequently used to build Single Page Apps (SPAs) that dynamically update the DOM as the user interacts with the page. It is tempting to populate some state in the JavaScript files so the JavaScript engine has some contextual information even before it has to load state from the server.
However, any website on the internet can import your generated JavaScript files. That means an attacker can build their
own malicious site and import your JavaScript code with a <script>
tag. The attacker will then be able to harvest the
sensitive details from your transpiled JavaScript for any victim that visits their malicious site. They might even link
to their malicious site in the comments section of your site to attract potential victims.
Mitigation
To avoid XSSI attacks, don’t interpolate sensitive data in JavaScript files - use JSON URLs instead, or encode the data in the HTML of page itself. JSON and HTML content types are subject to the browser’s same-origin policy, so they can’t be used in XSSI attacks.
Here’s how to safely initialize page state in React and Angular, by loading data from a JSON URL as the page is rendered.
React
// Retrieve configuration information from the server.
async componentDidMount() {
const response = await fetch('/api/config')
const data = await response.json()
this.setState({
loading : false,
user : data.user,
accessToken : data.accessToken
})
}
Angular
// The configuration information we will retrieve from the server.
export interface Config {
username : string
accessToken : string
role : string
}
@Injectable()
export class ConfigService {
constructor(private http: HttpClient) {}
// Retrieve configuration information from the server.
getConfig() {
return this.http.get<Config>('api/config')
.pipe(
catchError(this.handleError)
)
}
private handleError(error: HttpErrorResponse) {
if (error.status === 0) {
// A client-side or network error occurred. Handle it accordingly.
log.error('An error occurred:', error.error)
} else {
// The server returned an unsuccessful response code.
log.error(`Backend returned code ${error.status}, body was: `, error.error)
}
return throwError('An unexpected error occurred loading configuration.')
}
}