Welcome to the first practical lesson of the course! In the previous lesson, we learned that SAST (Static Application Security Testing) tools scan code "at rest" to find security flaws early in the development process. Now, we will move from theory to practice by configuring and using our first tool: ESLint. While many developers know ESLint as a tool for finding syntax errors or enforcing coding style, it becomes a powerful security scanner when we add the right plugins.
In this lesson, we will work with a "Pastebin-style" application written in TypeScript. We will configure ESLint to automatically detect dangerous coding patterns, specifically focusing on Object Injection and Regular Expression Denial of Service (ReDoS). You will learn how to set up the configuration file, run the scanner, interpret the warnings the tool generates, and, most importantly, rewrite the vulnerable code to make it secure.
To turn ESLint into a security tool, we need to tell it specifically what to look for. By default, ESLint only checks for standard JavaScript and TypeScript errors. We extend its capabilities by using a configuration file, typically named .eslintrc.cjs in modern JavaScript projects. This file acts as the instruction manual for the scanner, telling it which files to read and which rules to enforce.
In the CodeSignal environment, the necessary libraries are already installed for you. However, understanding the configuration is critical so you can replicate this setup in your own projects. We need to configure the parser to understand TypeScript syntax and load the security plugin.
Here is the basic structure of our configuration file:
The parser setting is essential because standard ESLint cannot read TypeScript's type definitions. The plugins array is where we activate eslint-plugin-security.
Both eslint-plugin-security and @typescript-eslint provide convenient recommended configurations that bundle their best-practice rules together and can be enabled with a single line in the extends array. Notably, the @typescript-eslint recommended config also disables core ESLint rules known to conflict with TypeScript parsing, saving you from subtle false positives. In a real project, you would typically extend both:
For this lesson, we will define our rules explicitly rather than using the recommended shorthand. This makes it clear exactly which rules are active and why, which is important for learning purposes.
After loading the plugin, we must explicitly tell ESLint which rules we want to enforce and how strictly to enforce them. In the rules section of our configuration, we define specific checks. We can assign a severity level to each rule: "warn" (which prints a yellow warning but allows the build to continue) or "error" (which prints a red error and typically stops the build process).
We will focus on a specific set of rules that target high-risk patterns in Node.js applications. These rules look for code that allows user input to control internal logic in unsafe ways.
Here is the configuration we will use for this lesson:
We set detect-object-injection to "warn" because, while it detects a very serious vulnerability, it also creates many False Positives. This means it might flag safe code just because it looks similar to a vulnerability. Setting it to "warn" ensures we see the potential issue without breaking the entire build process every time we use a square bracket.
While the configuration above includes several rules, this lesson focuses on just two of them: security/detect-object-injection and security/detect-non-literal-regexp. These two rules illustrate the most common vulnerability patterns you will encounter in typical Node.js applications, and they are good examples of both high-impact findings and the false positive challenge that comes with static analysis.
With the configuration in place, running the scanner is straightforward. From the root of your project, execute the following command in your terminal:
This tells ESLint to scan all .ts files inside the src directory. When issues are found, the output looks like this:
Each line in the report tells you the file path, the line and column number of the issue, the severity (warning or error), a short description of the problem, and the rule name that triggered it. You use the rule name to look up the exact vulnerability it represents and decide how to respond. In the sections below, we will walk through exactly what these two rules mean and how to fix the code they flag.
One of the most common issues flagged by our configuration is Object Injection (sometimes related to Prototype Pollution). This vulnerability occurs when an attacker can control the "key" used to access a property in an object. In JavaScript, all objects share a common structure called a "prototype." If an attacker can modify this prototype by injecting a malicious key (like __proto__), they might crash the application or even execute malicious code.
This matters because developers often use plain objects as simple caches or dictionaries. If you allow user input — like an ID from a web request — to be used directly as a key to look up data, you are opening the door to injection attacks. ESLint detects this pattern whenever it sees generic bracket notation like object[variable].
Let's look at a vulnerable example from our Pastebin application, where the developer tried to cache snippets to make the server faster:
In the code above, snippetCache is a plain JavaScript object. When the code runs snippetCache[id], it takes the id directly from the user's request. If a malicious user sends a request with a special key instead of a standard ID, they could potentially overwrite internal object methods. ESLint flags this with security/detect-object-injection because it sees a variable inside the square brackets.
To fix Object Injection vulnerabilities, we should avoid using plain JavaScript objects for caching or storing user-keyed data. Instead, modern JavaScript and TypeScript provide a specialized data structure called a Map. A Map is designed specifically for storing key-value pairs. Unlike a plain object, a Map does not have a prototype that can be polluted, and it safely separates the keys from the data structure itself.
Using a Map is the robust "defensive" coding solution. It tells the security scanner (and other developers) that we intend to store arbitrary keys and that we are doing so safely. Additionally, it is good practice to validate input before using it. If we expect an ID to be a UUID, we should check that it looks like a UUID before we process it.
Here is how we fix the vulnerable cache code:
By changing snippetCache to a new Map(), we replace the bracket notation snippetCache[id] with the method snippetCache.get(id). This method is secure by design. Even if the user sends __proto__ as the id, the Map will simply look for a key named __proto__ without trying to modify the object structure itself. This satisfies the security requirement and removes the ESLint warning.
When you run ESLint with security plugins, you will likely see a lot of warnings. It is important to understand that not every warning is a vulnerability. As we mentioned earlier, SAST tools lack context. ESLint sees a pattern that looks dangerous, but it cannot know if the data passing through that pattern is actually controlled by a malicious user.
When you see a warning like security/detect-object-injection, you must act as the investigator. Ask yourself: "Does the variable inside these brackets come from the user?"
- Yes: If it comes from
req.body,req.params, or an external API, it is a real vulnerability. You must fix it usingMapor strict validation. - No: If the variable comes from a hardcoded list of safe configuration keys, it is a
False Positive.
For example, iterating over a list of known constants is safe:
In professional environments, if you determine a finding is a false positive, you can disable the rule for that specific line using a comment like // eslint-disable-line security/detect-object-injection. However, for this course, we will focus on rewriting the code to be inherently secure, which is always the safer option.
In this lesson, we successfully configured ESLint to act as a security scanner for our TypeScript application. We learned how to set up the .eslintrc.cjs file, enable specific rules, and how both eslint-plugin-security and @typescript-eslint offer recommended configurations you can extend into your own projects. We also saw how to run the scanner with npx eslint "src/**/*.ts" and how to read its output.
Focusing on two of the configured rules, we explored detect-object-injection — which we fixed by switching from plain objects to Map — and detect-non-literal-regexp, which we prevented by escaping user input before creating regular expressions.
Now that you understand how to identify these issues and the logic behind fixing them, you are ready to try it yourself. In the upcoming practice exercises, you will be given a vulnerable codebase and tasked with configuring the scanner, interpreting the report, and applying the code fixes we discussed. Let's get started!
