In many standard enterprise applications, consistent logging serves a multitude of purposes. It helps businesses identify and rectify errors, provides valuable analytical insights, and lets you test new solutions. However, this also makes log injections one of the most common ways hackers can hijack or even gain access to sensitive user information. Vulnerabilities in the ways applications log data can allow attackers to inject malicious code into logs, compromising the integrity and confidentiality of your application.
In this article, you'll look specifically at log injection vulnerabilities in Node.js. You'll explore how log injections work and learn effective ways to prevent them.
What is a log injection vulnerability?
When it comes to logging information, some applications log to an external file or rely on an external API where logs are sent. A log injection occurs when an attacker manipulates input data to inject malicious code into the application's logs. These logs are typically used for debugging, monitoring, and auditing purposes.
When a log injection occurs, the attacker can clutter up the logs or even execute code within the context of the application, leading to various security risks.
Example of a vulnerable Node.js app
Consider a simple Node.js application that uses the Express web framework to log user-provided data without proper validation or sanitization. The following is a code snippet illustrating this vulnerability:
const express = require('express');
const app = express();
app.get('/log', (req, res) => {
const userInput = req.query.data;
console.log('INFO: ' + userInput);
res.send('Data logged successfully.');
});
app.listen(3000, () => {
console.log('Server started on port 3000');
});
Once this server is up and running locally, you can make a GET request using API testing tools like Insomnia to test whether things are working as expected. In this case, you're planning on logging a username from a form to test what the server logs look like. You add the data to match what your server is expecting and send the request. Doing so gives you a 200 OK
and some text that says, "Data logged successfully.":
If you check the logs on the server side, everything looks to be logging as expected. Since you added the value charlie
to the data parameters, the log shows INFO: charlie
being logged to the server:
One way a malicious person could misuse the way this application logs data is by inserting false logs via the data
parameter. In this example, all a malicious person would have to do is add additional text to the query:
For instance, a malicious person can add INFO: User deleted
as a false log. When the development team finds this log, they may be misdirected and spend time and resources trying to figure out why this application is deleting its users when a user submits a username, all because an attacker decided to insert additional characters to a user input form.
While this example is simplistic, it helps you understand what log injections are in general. Other types of injections can include adding false error logs or false user information logs simply by modifying the log message or, worse, inserting command injections that would grant attackers the ability to manipulate internal data directly.
How to prevent log injection vulnerability in Node.js
To mitigate log injection vulnerabilities in your Node.js applications, consider implementing some of these common practices:
Sanitize user inputs
In most cases, user inputs should be validated and sanitized before they're logged. This means ensuring that whatever is about to be logged has been checked thoroughly to exclude characters that would be mistaken for another log input and/or include only characters that should be in the logs.
Using the previous example, one way to sanitize logs would be to include a regex that filters and only allows alphabet letters, which would prevent special characters or numbers from being added to the log:
app.get('/log', (req, res) => {
const userInput = req.query.data
const sanitizedInput = userInput.replace(/[^a-zA-Z]+/, '')
console.log('INFO: Username: ', sanitizedInput);
res.send('Data logged successfully.');
});
One important caveat to remember is that choosing the right way to sanitize data can be tricky. In this example, someone with numbers in their username (eg 123) will have their username modified in the logs.
While sanitizing inputs can help catch malicious logs, the trade-offs require developers to be extra cautious about what they're filtering out and the ways they perform it. For example, incorrectly writing regular expressions can result in ReDOS vulnerabilities that cripple Node.js applications to a halt. Using third-party npm packages like validator.js
is a good option to avoid reinventing the wheel, but be sure to keep those dependencies up-to-date.
Be careful what you log
Every application is unique. One logging standard can't account for all the potential needs that may arise from each application. It's important to consider what information you need to log and for whom.
Logs that exist on the DEBUG level can help developers trace what is happening in their application and how the user is navigating from one component to the next or how information is flowing from one source to another. Logs that exist on the WARN or ERROR level can help engineering teams catch potential bugs early on and understand what is causing the application to crash or malfunction. The key here is to log only what you need and nothing more while considering what would happen if your logs are breached by attackers.
As this Snyk article on logging vulnerabilities explains, sensitive information sent to an external logging service can lead to data being breached by attackers. It's generally a good rule to avoid logging sensitive or confidential information, such as passwords, API keys, or personal data. If you do, make sure to implement proper access controls and consider using environment variables (such as on GitHub Actions) and key management systems to manage sensitive configuration data securely.
In addition to thinking through what you log, you need to consider how you log it. Structured logging can provide a more organized way of logging data that's easier to identify for developers. For instance, given the previous example, instead of logging the user data directly as a string, using a JSON object can allow you to label what gets logged in a cleaner way:
app.get('/log', (req, res) => {
const userInput = req.query.data;
const log = {username: userInput, 'action':'submitted username', 'time': new Date()}
console.log(log)
res.send('Data logged successfully.');
});
More importantly, structuring logs this way can make it more difficult for attackers to inject logs since there are many properties in the log object that the attacker would also need to manipulate (ie action
or time
), and not doing so can give away what the attacker is attempting to do:
While this logging of the JSON object is a simplistic example, there are logging libraries (discussed next) that provide structured logging as helpful alternatives to the usual console.log
.
Use a logging library instead of console.log
Many logging libraries like provide better control and security over log outputs compared to console.log
because they typically include built-in sanitization and filtering capabilities to prevent log injection attacks. They also provide developers with flags to control which logs show up in which environment (testing vs. production), flexibility regarding where the logs should be sent (local file, database, and/or third-party API), and ways to configure your logs that render them easier to trace.
One such library that we recommend is pino which is well-maintained and an overall healthy package with no known security vulnerabilities.
Pino is highly optimized for performance and can be set up to include timestamps in each log entry, it supports structured logging and standard system log levels. This added complexity and structure can enhance security, making it less likely for logs to be compromised by attackers.
Use snyk IDE extension for VS Code to detect vulnerabilities
Snyk offers a free IDE extension for Visual Studio Code (VS Code) that can help you detect log injection vulnerabilities (among other vulnerabilities) in your Node.js code. Here's how to set it up:
Navigate to the Extensions tab in VS Code and search for "Snyk" to find the "Snyk Security - Code, Open Source Dependencies, Iac Configurations" extension. Click Install:
Once installed, Snyk will ask you to authenticate your account by connecting to your IDE. Click Connect VS Code with Snyk, which opens up an external Snyk web page asking for your permission to connect. Once there, click Authenticate.
Once authenticated, click Scan to scan your code for vulnerabilities. You can also configure the extension as needed:
The Snyk IDE extension automatically scans your code for log injection vulnerabilities. Plus, Snyk highlights other potential security issues, including having hard-coded secrets or path traversal vulnerabilities, in your code during the scan and provides actionable insights and suggestions to help you secure your application.