5 best practices for React with TypeScript security

SnykSec - Dec 20 '22 - - Dev Community

As a library focused on building user interfaces rather than a full-fledged framework, React enables developers to choose their preferred libraries for various aspects of an application, such as routing, history, and authentication. Comparatively, Microsoft created TypeScript as an extension of JavaScript to introduce optional static typing to an otherwise loosely typed language.

Using TypeScript with React provides several advantages in application-building, including the option of simpler React components and better JavaScript XML (JSX) support for static type validation. As we can use JavaScript components in a TypeScript project, development teams with JavaScript experience can leverage this knowledge to benefit from strong-typing programming.

Many boilerplates are available for starter React projects, including Create React App, Create Next App, Vite, React Boilerplate, and React Starter Kit.

Create React App is a standalone tool that can run with either npm or Yarn. Once installed, we can generate and run a new project with just a few commands.

First, open your terminal and run the following command to install the Create React App tool:

npm install -g create-react-app
Enter fullscreen mode Exit fullscreen mode

Then, create a project using the TypeScript template with node package execute (npx):

npx create-react-app [webapp-name] --template typescript
Enter fullscreen mode Exit fullscreen mode

Alternatively, you can create the project using the Yarn package manager:

yarn create react-app [webapp-name] --template typescript
Enter fullscreen mode Exit fullscreen mode

potential security risks, and how to mitigate them.

Enable strict mode

Strict mode automatically turns on the TypeScript compiler parameters related to data type rules. When TypeScript strict mode is enabled, it validates the code using the strict type rules, forcing developers to write code respecting the limitations of the data types assigned to variables, constants, parameters, and function return values. Strict mode is important because it enables developers to catch and fix type mismatch bugs early in the development phase.

Suppose an application has the following function:

export async function deleteComment(slug, commentId): Promise<void> {
  await axios.delete(`articles/${slug}/comments/${commentId}`);
}
Enter fullscreen mode Exit fullscreen mode

We invoke the deleteComment function by passing the slug argument as a string and commentId as a number:

deleteComment('whats-new-in-react', 1257);
Enter fullscreen mode Exit fullscreen mode

However, this doesn’t prevent us from invoking the function with two strings:

deleteComment('foo', 'bar');
Enter fullscreen mode Exit fullscreen mode

The code above may not be allowed by business rules, but since we haven’t provided types, both parameters are assumed to be of the default any type. Consequently, TypeScript can’t help us identify the problem unless we enable strict mode.

Newly created React applications come with the strict value set to true. However, this value may differ for existing projects.

To ensure that our TypeScript project is running in strict mode, open the tsconfig.json file and check that the value of the strict configuration is true:

{
  ...
  "strict": true,
  ...
}
Enter fullscreen mode Exit fullscreen mode

Return to the deleteComment function. One small change immediately produces TypeScript compilation errors:

Add the string and number types to the deleteComment parameters:

export async function deleteComment(slug: string, commentId: number): Promise<void> {
  await axios.delete(`articles/${slug}/comments/${commentId}`);
}
Enter fullscreen mode Exit fullscreen mode

This produces compilation errors, preventing developers from creating client code that inadvertently calls the deleteComment function with invalid types, like in the example below:

deleteComment(null, true);
Enter fullscreen mode Exit fullscreen mode

The strict option automatically enables other recommended compiler options related to stricter type-checking.

Don’t use return type any in callbacks whose value will be ignored

If you declare a callback’s return type as any, then inadvertently use its return value when the function doesn’t return a value, you can make a mistake that goes undetected.

Consider a function named onListFieldKeyUp that takes a callback named onEnter as a parameter:

export function onListFieldKeyUp(onEnter: () => any): (ev: React.KeyboardEvent) => void {
  return (ev) => {
    if (ev.key === 'Enter') {
      ev.preventDefault();    
      var enterResult = onEnter();
      //do something with enterResult
      }
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

Note how the enterResult variable above stores the result of the onEnter callback function for further use. Although the callback doesn’t return a value, we declared the enterResult variable with type any, so TypeScript is unable to alert you to a problem.

What’s the solution to this?

First, if you know the onEnter function doesn’t return a value, replace the any type in the callback parameter with void. Now, TypeScript displays the error “An expression of type ‘void’ cannot be tested for truthiness.

Finally, remove the code that stores the return value of onEnter in the enterResult variable:

export function onListFieldKeyUp(onEnter: () => void): (ev: React.KeyboardEvent) => void {
  return (ev) => {
    if (ev.key === 'Enter') {
      ev.preventDefault();
      onEnter();
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

Server-side rendering attacks in React

A web application can render HTML on the client or the server. Modern JavaScript frameworks and libraries, like React, adopt server-side rendering. This approach provides performance benefits, including accelerated page loading — allowing the back end to quickly pre-render the entire page and pass the static HTML, CSS, and JavaScript content to the front end. As a result, users can navigate and see the web page instantly. This approach also helps with search engine optimization (SEO) since fast-loading pages reach higher scores in search algorithms.

Cross-site scripting (XSS) is an attack modality where attackers inject malicious client-side scripts into a web page. React was designed to be safe from XSS. However, improper programming and server-side rendering in React can lead to a XSS vulnerability that malicious users will exploit.

For example, never concatenate unsanitized data with the output of the renderToStaticMarkup function before sending the string to the client:

app.get("/", function (req, res) {
  return res.send(
    ReactDOMServer.renderToStaticMarkup(
      React.createElement("h1", null, "Hello World!")
    ) + someUnsanitizedData
  );
});
Enter fullscreen mode Exit fullscreen mode

The code above is unsafe because a hacker may have compromised the someUnsanitizedData variable to include malicious JavaScript code, like below:

someUnsanitizedData = "</scrïpt><scrïpt>alert('You are compromised!')</scrïpt>
Enter fullscreen mode Exit fullscreen mode

To prevent XSS attacks, use an HTML sanitizer such as DomPurify.

Use opaque types

An opaque data type enforces information-hiding. Its data structure is not defined in the interface, which hides and encapsulates the implementation of a concrete data type. While external modules can use the opaque type without accessing its internals, internal functions with access to the missing information can manipulate the type. Opaque types enable you to change and evolve internal details without changing the code that uses them. Therefore, using them remains a development best practice.

While TypeScript does not provide opaque types out of the box, we can implement them easily. Let’s try solving a real-world use case.

Imagine an e-commerce application using a function in TypeScript to add a product to the customer cart:

function addToCart(customerCode: string, productCode: string) {
  console.log(`Product ${productCode} has been added to the cart of the customer ${customerCode}`);
}
Enter fullscreen mode Exit fullscreen mode

These typed parameters ensure the programmer won’t provide numbers or other types in place of strings for the customerCode and productCode parameters:

addToCart('ABC-001984', 'SPC-004487');
Enter fullscreen mode Exit fullscreen mode

Though there isn’t anything wrong with the code, the type fails to precisely express the values. You can’t tell which parameter is the customer or product code just by looking at the code line, and TypeScript won’t give a warning if both values are swapped.

Now look at how the addToCart was refactored in the example below to use opaque types:

export type CustomerCode = string & { _: 'CustomerCode' };
export type ProductCode = string & { _: 'ProductCode' };

const makeCustomerCode =
  (customerCode: string): CustomerCode => {
    if (/^\w{3}-\d{6}$/.test(customerCode)) { //regex validation
      return customerCode as CustomerCode;
    } else {
      throw new Error('Not a customer code!');
    }
  };

const makeProductCode =
  (productCode: string): ProductCode => {
    if (/^\w{3}-\d{6}$/.test(productCode)) { //regex validation
      return productCode as ProductCode;
    } else {
      throw new Error('Not a product code!');
    }
  };

function addToCart(customerCode: CustomerCode, productCode: ProductCode) {
  console.log(`Product ${productCode} has been added to the cart of the customer ${customerCode}`);
}

let customerCode: CustomerCode = makeCustomerCode('ABC-001984');
let productCode: ProductCode = makeProductCode('SPC-004487');

addToCart(customerCode, productCode);
Enter fullscreen mode Exit fullscreen mode

Thanks to our new opaque types and their regex validation, the code produces a compiler error if you provide incorrect codes:

let customerCode: CustomerCode = makeCustomerCode('000-001984'); //Error: Not a customer code!
let productCode: ProductCode = makeProductCode('9871'); //Error: Not a product code!
Enter fullscreen mode Exit fullscreen mode

When to use dangerouslySetInnerHTML and observe proper sanitization practices

React uses a Virtual DOM as a lightweight strategy to update the page’s HTML efficiently, preventing users from having to deal with native browser APIs directly to manipulate HTML elements.

However, at times you may have to override this mechanism and set raw HTML code in your React applications. To directly manipulate this HTML code, React uses a special component property called dangerouslySetInnerHTML:

return (
<p dangerouslySetInnerHTML={{__html: data}}></p>);
Enter fullscreen mode Exit fullscreen mode

As the name suggests, the dangerouslySetInnerHTML property makes an application vulnerable if not used properly. Hackers can exploit applications relying on dangerouslySetInnerHTML to perform Cross-Site Scripting (XSS) attacks and inject malicious scripts disguised as trusted user input into your website.

To prevent this risk, always sanitize the HTML content before inserting it to eliminate “impurities” or malicious code. You can use a library like DOMPurify and apply the sanitize function:

import DOMPurify from 'dompurify';

return (
<p dangerouslySetInnerHTML={{__html: DOMPurify.sanitize(data)}}></p>);
Enter fullscreen mode Exit fullscreen mode

It’s crucial to note that, like any other library, DOMPurify is prone to the occasional security vulnerability. Fortunately, the Snyk vulnerability database quickly incorporated this vulnerability data and provided a solution. So, regardless of a library’s stable history, we should still practice our due diligence by using a tool like Snyk to periodically scan for any known vulnerabilities.

And that’s just one of the security concerns developers should consider. In this video, Liran Tal discusses how React developers can make mistakes, leading to other vulnerabilities that hackers can exploit.

Conclusion

In this article, we discussed some best practices for developing React applications with TypeScript, outlined potential security risks, and prescribed solutions.

  • Strict mode allows you to enforce type constraints and catch type mismatch mistakes early in development. Using void in callbacks prevents developers from using a return value from callbacks that don’t return a value.
  • Command injection attacks are a serious security threat. Avoid building dynamic code with suspicious user input and use the execFile function instead of exec.
  • HTML injections are another serious security threat. When using dangerouslySetInnerHTML, always sanitize the inserted markup to prevent attackers from successfully tampering with user input and injecting malicious code.
  • Finally, opaque types provide meaningful domains and help you more easily validate types and avoid duplicates.

It’s easy to use TypeScript and React to build fast, secure applications, especially using frameworks like Create React App and previous knowledge of JavaScript.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .