Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update qhelp for js/path-injection. #14846

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

max-schaefer
Copy link
Collaborator

Analogous to #14793.

@max-schaefer max-schaefer requested a review from a team as a code owner November 20, 2023 11:17
@max-schaefer max-schaefer added the no-change-note-required This PR does not need a change note label Nov 20, 2023
Copy link
Contributor

QHelp previews:

javascript/ql/src/Security/CWE-022/TaintedPath.qhelp

Uncontrolled data used in path expression

Accessing files using paths constructed from user-controlled data can allow an attacker to access unexpected resources. This can result in sensitive information being revealed or deleted, or an attacker being able to influence behavior by modifying unexpected files.

Recommendation

Validate user input before using it to construct a file path.

The choice of validation depends on whether you want to allow the user to specify complex paths with multiple components that may span multiple folders, or only simple filenames without a path component.

In the former case, a common strategy is to make sure that the constructed file path is contained within a safe root folder. First, normalize the path using path.resolve or fs.realpathSync to remove any ".." segments. Then check that the normalized path starts with the root folder. Note that the normalization step is important, since otherwise even a path that starts with the root folder could be used to access files outside the root folder.

In the latter case, you can use a library like the sanitize-filename npm package to eliminate any special characters from the file path. Note that it is not sufficient to only remove "../" sequences: for example, applying this filter to ".../...//" would still result in the string "../".

Finally, the simplest (but most restrictive) option is to use an allow list of safe patterns and make sure that the user input matches one of these patterns.

Example

In the first example, a file name is read from an HTTP request and then used to access a file within a root folder. However, a malicious user could enter a file name containing "../" segments to navigate outside the root folder and access sensitive files.

const fs = require('fs'),
      http = require('http'),
      url = require('url');

const ROOT = "/var/www/";

var server = http.createServer(function(req, res) {
  let filePath = url.parse(req.url, true).query.path;

  // BAD: This could read any file on the file system
  res.write(fs.readFileSync(ROOT + filePath, 'utf8'));
});

The second example shows how to fix this. First, the file name is resolved relative to a root folder, which has the side effect of normalizing the path and removing any "../" segments. Then, fs.realpathSync is used to resolve any symbolic links in the path. Finally, we check that the normalized path starts with the root folder's path, which ensures that the file is contained within the root folder.

const fs = require('fs'),
      http = require('http'),
      path = require('path'),
      url = require('url');

const ROOT = "/var/www/";

var server = http.createServer(function(req, res) {
  let filePath = url.parse(req.url, true).query.path;

  // GOOD: Verify that the file path is under the root directory
  filePath = fs.realpathSync(path.resolve(ROOT, filePath));
  if (!filePath.startsWith(ROOT)) {
    res.statusCode = 403;
    res.end();
    return;
  }
  res.write(fs.readFileSync(filePath, 'utf8'));
});

References

@max-schaefer max-schaefer added the ready-for-doc-review This PR requires and is ready for review from the GitHub docs team. label Nov 20, 2023
Copy link
Contributor

@sabrowning1 sabrowning1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👋🏼 @max-schaefer, thanks for making these changes! I've left a few comments below, but they're all pretty small tweaks. Let me know if any of my suggestions don't make sense 🙂

Comment on lines +26 to +27
Then check that the normalized path starts with the root folder.
Note that the normalization step is important, since otherwise even a path that starts with the root folder could be used to access files outside the root folder.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Then check that the normalized path starts with the root folder.
Note that the normalization step is important, since otherwise even a path that starts with the root folder could be used to access files outside the root folder.
You should always normalize the file path since an unnormalized path that starts with the root folder can still be used to access files outside the root folder.
Then, after you have normalized the path, check that the path starts with the root folder.

Some small suggestions here to group the normalization step with the explanation of its importance, and to maintain context for ease of localization.

</p>

<p>
Ideally, follow these rules:
The choice of validation depends on whether you want to allow the user to specify complex paths with multiple components that may span multiple folders, or only simple filenames without a path component.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The choice of validation depends on whether you want to allow the user to specify complex paths with multiple components that may span multiple folders, or only simple filenames without a path component.
The validation method you should use depends on whether you want to allow the user to specify complex paths with multiple components that may span multiple folders, or only simple filenames without a path component.

Small nit for clarity

In the first example, a file name is read from an HTTP request and then used to access a file.
However, a malicious user could enter a file name which is an absolute path, such as
<code>"/etc/passwd"</code>.
In the first example, a file name is read from an HTTP request and then used to access a file within a root folder.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
In the first example, a file name is read from an HTTP request and then used to access a file within a root folder.
In the first (bad) example, the code reads the file name from an HTTP request, then accesses that file within a root folder.

Small edit to remove passive voice and be extra clear that this example is bad 😁

However, a malicious user could enter a file name which is an absolute path, such as
<code>"/etc/passwd"</code>.
In the first example, a file name is read from an HTTP request and then used to access a file within a root folder.
However, a malicious user could enter a file name containing "../" segments to navigate outside the root folder and access sensitive files.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
However, a malicious user could enter a file name containing "../" segments to navigate outside the root folder and access sensitive files.
A malicious user could enter a file name containing "../" segments to navigate outside the root folder and access sensitive files.

Small tweak to align with the edits to the previous sentence

reading the file located at <code>"/home/user/../../etc/passwd"</code>, which is the system's
password file. This file would then be sent back to the user, giving them access to all the
system's passwords.
The second example shows how to fix this.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The second example shows how to fix this.
The second (good) example shows how to avoid access to sensitive files by sanitizing the file path.

Another tiny change to be extra clear and add context for localization

Comment on lines +50 to +52
First, the file name is resolved relative to a root folder, which has the side effect of normalizing the path and removing any "../" segments.
Then, <code>fs.realpathSync</code> is used to resolve any symbolic links in the path.
Finally, we check that the normalized path starts with the root folder's path, which ensures that the file is contained within the root folder.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
First, the file name is resolved relative to a root folder, which has the side effect of normalizing the path and removing any "../" segments.
Then, <code>fs.realpathSync</code> is used to resolve any symbolic links in the path.
Finally, we check that the normalized path starts with the root folder's path, which ensures that the file is contained within the root folder.
First, the code resolves the file name relative to a root folder, normalizing the path and removing any "../" segments in the process.
Then, the code calls <code>fs.realpathSync</code> to resolve any symbolic links in the path.
Finally, the code checks that the normalized path starts with the path of the root folder, ensuring the file is contained within the root folder.

A few small edits here to remove passive voice

var server = http.createServer(function(req, res) {
let filePath = url.parse(req.url, true).query.path;

// BAD: This could read any file on the file system
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// BAD: This could read any file on the file system
// BAD: This function uses unsanitized input that can read any file on the file system.

Small suggestion for extra context/ease of localization

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation JS no-change-note-required This PR does not need a change note ready-for-doc-review This PR requires and is ready for review from the GitHub docs team.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants