[WebSec for Front-End: Building Secure Web Apps]

Summary

The article highlights common security attacks in web development and best practices for mitigating these risks. It covers both front-end and back-end aspects of web security, defines major rules, and provides a practical guide with a demo project to showcase basic web security concepts. Key areas discussed include authentication and session management, access control, input validation, cross-site attacks, network-level attacks, webpage and content manipulation, and vulnerabilities in third-party code.

Intro

Frameworks like React, Angular, and Vue.js provide robust security features. But you'd better not to rely on anyone but yourself. The hackers and developers are in the constant cat-and-mouse game, so it's by no means an extensive guide. We'll just try to highlight widely known attacks and best practice to prevent them.

Common security attacks

Authentication and session management

Broken authentication – authentication mechanism isn't strong enough, allowing attackers to bypass protective measures and get unauthorized access.

Example: Ticketmaster data breach in 2018 resulted in theft of 40,000 individuals' credit card information. Hackers managed to infiltrate the system by exploiting malicious software present on a customer support product.

Cookie theft – stealing cookies to impersonate users.

Session hijacking – taking over a user session to gain access to user's information or services.

Example: Firesheep web extension allowed users to hijack Facebook sessions by capturing cookies transmitted over unencrypted connections on shared Wi-Fi networks.

Access control

Broken access control – user can access data or perform actions they shouldn't be able to, due to failures in access control.

Example: The Google Docs sharing bug in 2009 allowed users to access documents and presentations even if they hadn't explicit permission.

  • Improper implementation of blacklist and whitelist: not effectively blocking or allowing access.
  • Role-based access control (RBAC) issues: incorrect role definitions leading to unauthorized access.
  • Ownership flaws: wrong checking if a user owns or has rights to a resource.
  • Directory traversal attack: access directories outside the intended directory structure, allowing an attacker to access restricted files or execute commands on the server in certain cases.

Input validation and injection attacks

SQL injection: exploiting poor input validation to execute unauthorized SQL queries on server database.

Example: In 2011, 77 million PlayStation Network accounts were compromised with SQL injection.

Remote code execution (RCE): injecting malicious code into vulnerable applications.

Arbitrary file upload vulnerability: allow an attacker to upload malicious files to a server. These files can be crafted to execute unauthorized commands, exploit vulnerabilities, or compromise server security. E.g., an SVG file could contain embedded JavaScript for XSS attacks, a PHP file could enable remote code execution (RCE), and an ASPX file could also be used for RCE.

Cross-site attacks

Cross-site scripting (XSS) – inject malicious scripts into web pages viewed by users.

Example: In 2017, a TweetDeck XSS vulnerability allowed attackers to execute JS code in the browsers of Twitter users. The bug was known since 2011, several times Twitter claimed they have fixed it but turned out, not completely.

Stored XSS: The script is permanently stored on the target servers.

Reflected XSS: The script is reflected off the web server in an error message, search result, or any response that includes some or all of the input sent to the server as part of the request.

DOM-based XSS: Malicious JS was injected into DOM by an attacker due to the use of insecure methods such as innerHTML, document.write, location.href, location.hash, eval, and setTimeout by the developers.

Use cases: Credentials theft, Keylogging, Geolocation stealing, Cryptomining.

Cross-site request forgery (CSRF) – trick users into executing actions on websites where they're currently authenticated without user's knowledge or consent.

Example: In 2007, hackers exploited Netflix's lack of CSRF protections and manipulated users into unknowingly rating movies according to the attacker's preference.

Clickjacking – tricking users into clicking something different than they perceive to redirect them to malicious resources or steal sensitive information.

Example: In 2008, white-hat hackers discovered that Adobe Flash player settings can be framebusted with a clicker game while the actual settings are covered by an invisible iframe. Users unknowingly clicked buttons that enabled game an access to webcam and microphone.

Network-level attacks

Man-in-the-middle (MitM) – intercepting communication between client and servers to steal or manipulate data.

Example: In 2013, Nokia's Xpress Browser was revealed to be decrypting HTTPS traffic on Nokia's proxy servers, giving the company clear text access to its customers' encrypted browser traffic.

Web cache poisoning – manipulate the caching mechanism to provide tampered or “poisoned” content to users instead of the valid cache.

Example: In 2022, security researcher Iustin Ladunca discovered 70 cache poisoning vulnerabilities across services, including GitHub and GitLab.

Distributed denial of service (DDoS) – overwhelming a service with high traffic, usually using bots.

Example: In 2016, the Mirai botnet launched massive DDoS attack against the DNS provider Dyn, disrupting access to major websites like Twitter, Netflix, and PayPal.

Webpage and content manipulation

iFrame-based attacks – placing your app in iframe of a malicious website. Good example of malicious iframes.

Phishing – deceiving users into giving away personal information, often through fake emails to company's employees, building copycats of the targeted app. Check out a library of phishing websites for reference.

Third-party and code-level vulnerabilities

Third-party code – exploiting vulnerabilities in external libraries or services integrated into an app.

Example: In 2021, vulnerability in Java-logging library Log4j (used by Amazon, Microsoft, IBM and Google) versions 2.14.1 and below allowed hackers to capture logs possibly containing personal data like passwords.

In 2024 a maliciously introduced backdoor was found in xz, open-source data compression tool, which is used by major Linux distros, like Debian, Fedora, and Kali. This backdoor allowed the hacker to execute code on any machine that was running mentioned Linux distros.

Source code disclosure – exposure of source code to unauthorized users.

Sensitive data exposure – inadequate protection of sensitive data like API keys, tokens, passwords. Usually it happens due to hard-coded credentials, insecure APIs or client-side logging.

Example: In 2023, researchers discovered that Mercedes-Benz employee exposed their authentication token in a public GitHub repository, granting unrestricted access to company’s source code.

Most vulnerable areas at front-end

Login page logic

  • Vulnerabilities: Brute force attacks, credential stuffing, phishing, and DDoS.
  • Mitigation: Implement CAPTCHA, MFA, HTTPS, account lockout mechanisms after several failed attempts. Don't use local storage for store credentials on the client

User-controlled input and forms

  • Vulnerabilities: XSS, injection attacks.
  • Mitigation: Sanitize and validate all user input, restrict allowed media formats, use prepared statements for database queries, and restrict file types and sizes for uploads.

Data transfer (client to server)

  • Vulnerabilities: Man-in-the-middle attacks.
  • Mitigation: Use TLS/SSL for all data transit, implement HSTS (HTTP Strict Transport Security), and consider encrypting sensitive data before transmission.

Search and error pages

  • Vulnerabilities: Reflected XSS, SQL Injection, and information leakage through detailed error messages.
  • Mitigation: Make sure your server responds with properly sanitized user inputs, employs CSP headers, and returns very generic error messages.
  • Vulnerabilities: Phishing via malicious links, malvertising, clickjacking, CSRF.
  • Mitigation: Implement CSRF tokens and thoroughly vet ad networks or third-party scripts.

Third-party services and libraries

  • Vulnerabilities: attacks on third-party components, dependency confusion.
  • Mitigation: Regularly update dependencies, use tools to track vulnerable dependencies, and ensure secure configuration of all third-party services.

Basic cybersecurity policies

Same-origin policy (SOP): ensures that a web page can only interact with resources from the same origin, unless explicit permission is given by the server. Default browser's behavior.

Cross-origin resource sharing (CORS): a way to relax SOP's strict rules. CORS allows web apps to make requests to domains other than their own. Setting example: Access-Control-Allow-Origin: "https://www.example.com"

Content security policy (CSP): Allows developers to specify which sources the browser should allow to load content for a page. Setting example: Content-Security-Policy: default-src 'self' *.allowed-domain.com

General rules

Web security:

  • Always use HTTPS for production
  • Secure your configuration and never expose your credentials.
  • Principle of least privilege: anything which is not allowed explicitly should be prohibited.
  • Generic error messages: don’t give attackers any useful information.
  • Follow REST principles: e.g. GET requests shouldn’t cause side effects on server.
  • Use anti-CSRF tokens (either client-side or server-side). Check out a guide on anti-CSRF tokens
  • Set attributes Secure, HttpOnly, SameSite to cookies.
Set-Cookie: Secure; HttpOnly; SameSite=Strict; Expires=Thu, 21 Oct 2024 07:28:00 GMT
  • Restrict iFrame usage: configure your CSP to disallow embedding your app in iFrames on other sites. E.g. Content-Security-Policy: frame-ancestors 'none'
  • Setup reporting: scanning and reporting tools for errors and security breaches, like Sentry, Snyk, and ZAP
  • Use static code analysis tools to identify security risks: eslint plugins eslint-security and eslint-no-unsanitized
  • Check dependency security level using snyk before installing it.

Front-end security:

  • Use robust third-party auth services, along with MFA and CAPTCHA.
  • Encourage users to set strong passwords. Block sign-in attempts after several failures in a row
  • Validate and sanitize HTML input, using tools like DOMPurify.
  • Be cautious about file uploading, especially SVG, XML, ZIP and CSV.
  • Scrutinize redirects and ads.
  • Avoid serialization of confidential data. Make sure to sanitize and encrypt it before sending to server.
  • Obfuscate source code: minify and obfuscate JS during the build, restrict access to source maps.
  • Secure your dependencies: choose them wisely, follow developers' social media to stay updated, do audits, set integrity and crossorigin attributes to script and link tags.
<script 
  src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
  integrity="sha384-q8i/X+965..."
  crossorigin="use-credentials"> // or "anonymous"
</script>
  • Run npm update from time to time to update all your dependencies to the latest versions
  • When writing in vanilla JS, avoid inline JS in onclick or href to mitigate risk of XSS attacks. Instead, use event listeners.
  • Handle localStorage and sessionStorage carefully, storing only non-sensitive information, and consider encryption.
  • Conduct security testing: penetration testing, and automated scans

Practical guide

Clone this small full-stack project and experiment with it.

img1

The above mentioned project is just a demonstration of a basic setup, so the security measures here are mere placeholders.

At the front-end:

1. Set security-related meta-tags in your HTML

<!-- Visit https://content-security-policy.com/ to check all directives -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; object-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self'; media-src 'self'; frame-src 'none'; font-src 'self';">
<!-- Referrer Policy - Controls information sent along with requests -->
<meta name="referrer" content="no-referrer">

2. Write axios interceptors for your requests to handle certain errors

baseAxios.interceptors.response.use(
    response => response,
    (errorAxiosInstance) => {
      // If the error response status is 401, try refreshing the token
      if (errorAxiosInstance.response && errorAxiosInstance.response.status === 401) {
        return refreshAuthToken(errorAxiosInstance.config);
      }
      // For other errors, reject the promise
      return Promise.reject(errorAxiosInstance);
    },
  );

3. Never trust user input. Sanitize and encrypt payload before submitting it to the server, rendering in DOM or passing to another components

const postRequest = async (endpoint, payload) => {
    // Use DOMPurify to sanitize user's input at Front-End
    const sanitizedPayload = sanitizePayload(payload);
    try {
      return await baseAxios.post(endpoint, { data: getEncrypted(JSON.stringify(sanitizedPayload)) });
    } 
    ...
  }

Sanitation:

export const sanitizePayload = (payload) => {
  const sanitizedPayload = {};
  for (const key in payload) {
    if (payload.hasOwnProperty(key) && typeof payload[key] === 'string')
      sanitizedPayload[key] = sanitizeString(payload[key]);
    else
      sanitizedPayload[key] = payload[key];
  }
  return sanitizedPayload;
};

Encryption:

export const getEncrypted = (plaintextData) => {
 const encryptionKey = import.meta.env.VITE_ENCRYPTION_KEY;
 return CryptoJS.AES.encrypt(plaintextData, encryptionKey).toString()
};

4. Be careful with JSON.stringify(), since it converts any data into a string without detecting malicious values. Also, you can't stringify an object if it contains circular references.

At the back-end:

1. Use standard auth protocols for your app: OAuth, JSON Web Token (JWT), etc.

2. Set security headers for cookies you send in the response.

const cookieConfig = {
  httpOnly: true, // Cookie is accessible only through HTTP requests, not JS
  secure: true, // Cookie should only be sent over HTTPS
  sameSite: 'strict', // Cookie should only be sent in first-party context and not sent along with cross-site requests
};
Set-Cookie: cookieKey=cookieValue; HttpOnly; Secure; SameSite=Strict | Lax

3. Use http cookies to transfer refresh and access tokens between front-end and back-end.

// on login, set the tokens to response cookies
app.post('/api/login', (req, res) => {
  ...
  res.cookie('AUTH-TOKEN', authToken, cookieConfig);
  res.cookie('REFRESH-TOKEN', refreshToken, cookieConfig);
  return res.status(200).send('Success');
}
// on subsequent requests, check for token in the request cookies
const checkAuth = async (req, res, next) => {
  // Extract auth token from cookies
  const token = req.cookies['AUTH-TOKEN'];
  if (!token) {
    res.status(401).send('Please authenticate');
    return;
  }
  ...
}

4. Configure CSP to your needs.

Content-Security-Policy: script-src 'self' \*.your-domain.com https://www.google-analytics.com

5. Enforce use of HTTPS only.

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

6. Setup CSRF.

const csrfProtection = csurf({ cookie: true, value: req => req.cookies['CSRF-TOKEN'] });
app.use((req, res, next) => {
  // Bypass CSRF protection on /api/login, since user doesn't have CSRF token on login page yet
  if (req.path === '/api/login') next();
  // Apply CSRF protection to all other routes
  else {
    csrfProtection(req, res, (err) => {
      if (err) next(err);
      else {
        const csrfToken = req.csrfToken();
        res.cookie('CSRF-TOKEN', csrfToken, cookieConfig);
        next();
      }
    });
  }
});

7. Prevent use of iframes.

X-Frame-Options: SAMEORIGIN

8. Disable 3-rd party services from using camera and microphone on your platform (e.g. in case you do use iframes).

Feature-Policy: camera=(); microphone=();

9. Prevent MIME sniffing.

X-Content-Type-Options: nosniff

10. Hide your web server version in responses and error messages.

Conclusion

It's practically impossible to make your app hack-proof, but our goal is just to make it a less easy target. The bare minimum for survival can be summarized as: trust nobody but yourself, never expose anything unintentionally, and monitor vulnerability reports in your dependencies. Learn from past developer mistakes and strive for a balance between over-engineered security paranoia and complete lack of defense.

References

McDonald M. Web Security for Developers. San Francisco: No Starch Press, Inc. - 2020 - 218 p.

Front-End Security Best Practices

Front-End Security: 10 Popular Types of Attacks and Best Practices to Prevent Them

6 Tips to Harden Your HTTP Headers

Demystifying Security in Front-end Development: A Comprehensive Overview

10 React Security Best Practices

How to Secure Your React.js Application

React Security Vulnerabilities and How to Fix/Prevent Them

React.js Security Guide: Threats, Vulnerabilities, and Ways to Fix Them

The Hard Parts of JWT Security Nobody Talks About

Content-Security-Policy (CSP) Header Quick Reference