Cross-Site Script Inclusion

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

Prevalence Occasional
Exploitability Easy
Impact Harmful

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, which are subject to the browser’s same-origin policy. Here’s how to safely initialize page state in React and Angular, but loading data from a JSON URL as the page is rendered.

// 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
  })
}
// 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.')
  }
}

Further Reading