Back To Posts
JavaScript Security Best Practices

Sanket Joshi | September 18, 2024

JavaScript Security Best Practices

JavaScript (JS) applications can be targeted by cybercriminals in many ways, particularly by exploiting client-side execution of JS with a range of tactics and techniques. Let’s quickly recap some of the most common JavaScript vulnerabilities that need to be protected against:

Common JavaScript Vulnerabilities

  • Cross-Site Scripting (XSS): Malicious scripts are injected into a vulnerable application or website, allowing hackers to manipulate what is returned by the web browser.
  • Man-in-the-Middle Attacks (MitM): A hacker positions themselves between an application and the user to intercept sensitive data.
  • Denial of Service Attacks (DoS): An attack that floods a server with numerous requests to take an application offline.
  • Cross-Site Request Forgery (CSRF): A malicious exploit that tricks an authorized user into performing unintended actions, such as submitting financial transactions.
  • Session Hijacking: A hacker steals a user’s session ID to hijack an active session.

5 Essential Security Best Practices for JavaScript Development

1. Securing APIs

Many APIs, especially in Node.js, are built using REST architecture. Key considerations for securing REST APIs include:

  • Use HTTPS: Prevent unauthorized access by always using HTTPS.

    const express = require('express');
    const https = require('https');
    const fs = require('fs');
    
    const app = express();
    
    // SSL Configuration for HTTPS
    const sslOptions = {
      key: fs.readFileSync('private-key.pem'),
      cert: fs.readFileSync('certificate.pem')
    };
    
    https.createServer(sslOptions, app).listen(443, () => {
      console.log('HTTPS Server running on port 443');
    });
    
  • Access Control Lists (ACLs): Restrict access to authorized users.

  • Authentication: Implement methods like API keys, OAuth, or JWT for secure access.

    const jwt = require('jsonwebtoken');
    
    // Middleware for checking JWT token
    function authenticateToken(req, res, next) {
      const token = req.headers['authorization'];
      if (!token) return res.sendStatus(403);
      
      jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
        if (err) return res.sendStatus(403);
        req.user = user;
        next();
      });
    }
    
  • Input Validation: Ensure only valid data is processed by the API to prevent malicious input.

    // Example of input validation using Express.js
    const { body, validationResult } = require('express-validator');
    
    app.post('/submit', [
      body('email').isEmail(),
      body('password').isLength({ min: 6 })
    ], (req, res) => {
      const errors = validationResult(req);
      if (!errors.isEmpty()) {
        return res.status(400).json({ errors: errors.array() });
      }
      // Process valid input
    });
    

2. Implementing Content Security Policies (CSP)

A Content Security Policy (CSP) is a browser security standard that controls which resources the browser is allowed to load. This prevents XSS vulnerabilities and potential data breaches.

  • CSP Headers or Meta-Tags: Define what content the browser can load.

    <!-- Example CSP Meta Tag -->
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://trustedscripts.example.com;">
    
  • CSP Directives: Further control which domains can load specific resources.

    // Setting CSP header in Express.js
    app.use((req, res, next) => {
      res.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'self' https://trustedscripts.example.com");
      next();
    });
    

3. Input Sanitization

Input sanitization refers to cleaning and validating user-provided data to prevent the execution of malicious code. This not only enhances security but also improves application performance and reduces input errors.

  • Escape User Input: Encode special characters to prevent XSS attacks.

    function sanitizeInput(input) {
      return input
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&#039;");
    }
    
    const userInput = sanitizeInput("<script>alert('XSS');</script>");
    
  • Input Validation: Ensure that input data is always in the correct format.

    // Example of input validation using regex
    function validateUsername(username) {
      const usernameRegex = /^[a-zA-Z0-9_]{3,20}$/;
      return usernameRegex.test(username);
    }
    
    console.log(validateUsername("Valid_User123")); // true
    console.log(validateUsername("<script>")); // false
    

4. Preventing Cross-Site Scripting (XSS) Attacks

Prevent XSS attacks by following these steps:

  • Sanitize and Validate Input: Ensure only allowed characters are used.

  • Encoding Input: Convert special characters into HTML entities to prevent execution.

    function encodeForHTML(input) {
      const element = document.createElement('div');
      element.innerText = input;
      return element.innerHTML;
    }
    
    const safeContent = encodeForHTML("<script>alert('XSS');</script>");
    
  • Use HTTP-Only Cookies: Prevent client-side JavaScript from accessing cookies by using HTTP-only flags.

    // Example in Express.js to set HTTP-only cookie
    res.cookie('sessionID', 'abc123', { httpOnly: true });
    

5. Regular Security Audits

Security audits are crucial for maintaining a secure JavaScript application. A typical audit involves:

  • Check Dependencies: Use tools like Dependabot to stay updated on security patches.

    # Dependabot config in a .yaml file
    version: 2
    updates:
      - package-ecosystem: "npm"
        directory: "/"
        schedule:
          interval: "weekly"
    
  • Validate Input and Sanitization: Ensure these are functioning as intended.

  • Hide Environment Variables: Ensure no sensitive data is exposed client-side.

    // Example of hiding environment variables
    require('dotenv').config();
    const secretKey = process.env.SECRET_KEY;
    
  • Implement Security Headers: Include CSP, HSTS, X-Content-Type-Options, Permissions-Policy, and Referrer-Policy headers.

    app.use((req, res, next) => {
      res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
      res.setHeader('X-Content-Type-Options', 'nosniff');
      res.setHeader('Referrer-Policy', 'no-referrer');
      next();
    });
    
  • Centralize Key Functions: Streamline testing, auditing, and maintenance.

  • Use Linting and Static Analysis Tools: Leverage built-in security tools to identify potential issues early.

    # Example of using ESLint for static code analysis
    npx eslint yourfile.js
    

Web Application Security Tools

  • Snyk: A developer-first security platform that automatically identifies vulnerabilities in JavaScript code, dependencies, and containers.

  • Zed Attack Proxy (ZAP) by OWASP: An open-source penetration testing tool for web applications, supporting both automated and manual testing.

  • Cypress Testing Framework: Preferred over other JavaScript testing frameworks due to its fast execution, real-time processing, visual debugging, and API testing capabilities.

    # Running Cypress test
    npx cypress open