Skip to content

Commit

Permalink
feat(python): path traversal with user input (CWE 73) (#395)
Browse files Browse the repository at this point in the history
  • Loading branch information
elsapet committed May 15, 2024
1 parent d084574 commit 16f2fae
Show file tree
Hide file tree
Showing 6 changed files with 360 additions and 0 deletions.
72 changes: 72 additions & 0 deletions rules/python/django/path_using_user_input.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
imports:
- python_shared_common_user_input
- python_shared_lang_import4
patterns:
- pattern: $<FILE_SYSTEM_STORAGE>($<...>$<USER_INPUT>$<...>)
filters:
- variable: USER_INPUT
detection: python_shared_common_user_input
scope: result
- variable: FILE_SYSTEM_STORAGE
detection: python_shared_lang_import4
scope: cursor
filters:
- variable: MODULE1
values: [django]
- variable: MODULE2
values: [core]
- variable: MODULE3
values: [files]
- variable: MODULE4
values: [storage]
- variable: NAME
values: [FileSystemStorage]
- pattern: $<DEFAULT_STORAGE>.save($<USER_INPUT>, $<...>)
filters:
- variable: DEFAULT_STORAGE
detection: python_shared_lang_import4
scope: cursor
filters:
- variable: MODULE1
values: [django]
- variable: MODULE2
values: [core]
- variable: MODULE3
values: [files]
- variable: MODULE4
values: [storage]
- variable: NAME
values: [default_storage]
- variable: USER_INPUT
detection: python_shared_common_user_input
scope: result
languages:
- python
severity: high
metadata:
description: Unsanitized user input in file path
remediation_message: |-
## Description
Unsanitized user input in file path resolution can lead to security vulnerabilities. This issue arises when an application directly uses input from the user to determine file paths or names without proper validation or sanitization. Attackers can exploit this to access unauthorized files or directories, leading to data breaches or other security compromises.
## Remediations
- **Do not** directly use user input in file paths without sanitization. This prevents attackers from manipulating file paths to access or manipulate unauthorized files.
- **Do** use a safelist to define accessible paths or directories. Only allow user input to influence file paths within these predefined, safe boundaries.
- **Do** sanitize user input used in file path resolution. For example, use absolute paths and check against the expected base directory
```python
BASE_DIRECTORY = '/path/to/safe/directory'
my_path = os.path.abspath(os.path.join(BASE_DIRECTORY, user_input))
if my_path.startswith(BASE_DIRECTORY):
open(my_path)
```
- **Do not** use user input when creating an instance of a file storage class such as `FileSystemStorage`. Rather rely on the default configuration as set in `settings.MEDIA_ROOT`
```python
storage = FileSystemStorage(user_input) # unsafe
```
cwe_id:
- 73
id: python_django_path_using_user_input
documentation_url: https://docs.bearer.com/reference/rules/python_django_path_using_user_input
213 changes: 213 additions & 0 deletions rules/python/lang/path_using_user_input.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
imports:
- python_shared_common_user_input
- python_shared_lang_import1
- python_shared_lang_import2
patterns:
- pattern: open($<USER_INPUT>$<...>)
filters:
- variable: USER_INPUT
detection: python_shared_common_user_input
scope: result
- pattern: $<FILEINPUT>($<...>files=$<USER_INPUT>$<...>)
filters:
- variable: FILEINPUT
detection: python_shared_lang_import1
scope: cursor
filters:
- variable: MODULE1
values: [fileinput]
- variable: NAME
values:
- input
- FileInput
- variable: USER_INPUT
detection: python_shared_common_user_input
scope: result
- pattern: $<IO>($<USER_INPUT>$<...>)
filters:
- variable: USER_INPUT
detection: python_shared_common_user_input
scope: result
- variable: IO
detection: python_shared_lang_import1
scope: cursor
filters:
- variable: MODULE1
values: [io]
- variable: NAME
values:
- open
- open_code
- pattern: $<OS>($<USER_INPUT>)
filters:
- variable: USER_INPUT
detection: python_shared_common_user_input
scope: result
- variable: OS
detection: python_shared_lang_import1
scope: cursor
filters:
- variable: MODULE1
values: [os]
- variable: NAME
values:
- listdir
- chdir
- mkdir
- makedirs
- open
- rmdir
- remove
- rename
- unlink
- pattern: $<OS_PATH>($<USER_INPUT>)
filters:
- variable: USER_INPUT
detection: python_shared_common_user_input
scope: result
- variable: OS_PATH
detection: python_shared_lang_import2
scope: cursor
filters:
- variable: MODULE1
values: [os]
- variable: MODULE2
values: [path]
- variable: NAME
values: [join]
- pattern: $<SHUTIL>($<SOURCE>, $<DEST>, $<...>)
filters:
- either:
- variable: SOURCE
detection: python_shared_common_user_input
scope: result
- variable: DEST
detection: python_shared_common_user_input
scope: result
- variable: SHUTIL
detection: python_shared_lang_import1
scope: cursor
filters:
- variable: MODULE1
values: [shutil]
- variable: NAME
values:
- copy
- copy2
- copyfile
- copymode
- copystat
- copytree
- pattern: $<SHUTIL>($<USER_INPUT>$<...>)
filters:
- variable: USER_INPUT
detection: python_shared_common_user_input
scope: result
- variable: SHUTIL
detection: python_shared_lang_import1
scope: cursor
filters:
- variable: MODULE1
values: [shutil]
- variable: NAME
values: [rmtree]
- pattern: $<PATH>.$<METHOD>($<...>)
filters:
- variable: PATH
detection: python_lang_path_using_user_input_path_module_init_with_user_input
scope: result
- variable: METHOD
values:
- joinpath
- mkdir
- open
- read_bytes
- read_text
- rename
- replace
- rmdir
- symlink_to
- touch
- unlink
- walk
- write_bytes
- write_text
- pattern: $<PATH>.$<METHOD>($<USER_INPUT>)
filters:
- variable: USER_INPUT
detection: python_shared_common_user_input
scope: result
- variable: PATH
detection: python_lang_path_using_user_input_path_module_init_without_user_input
scope: result
- variable: METHOD
values:
- joinpath
- rename
- replace
- symlink_to
- touch
auxiliary:
- id: python_lang_path_using_user_input_path_module_init_without_user_input
patterns:
- pattern: $<PATH_MODULE>
filters:
- variable: PATH_MODULE
detection: python_lang_path_using_user_input_path_module
- pattern: $<PATH_MODULE>($<...>)
filters:
- variable: PATH_MODULE
detection: python_lang_path_using_user_input_path_module
- id: python_lang_path_using_user_input_path_module_init_with_user_input
patterns:
- pattern: $<PATH_MODULE>($<USER_INPUT>)
filters:
- variable: PATH_MODULE
detection: python_lang_path_using_user_input_path_module
- variable: USER_INPUT
detection: python_shared_common_user_input
scope: result
- id: python_lang_path_using_user_input_path_module
patterns:
- pattern: $<PATH_MODULE>
filters:
- variable: PATH_MODULE
detection: python_shared_lang_import1
scope: cursor
filters:
- variable: MODULE1
values: [pathlib]
- variable: NAME
values:
- Path
- PurePath
- WindowsPath
- PureWindowsPath
- PosixPath
- PurePosixPath
languages:
- python
severity: high
metadata:
description: Unsanitized user input in file path
remediation_message: |-
## Description
Unsanitized user input in file path resolution can lead to security vulnerabilities. This issue arises when an application directly uses input from the user to determine file paths or names without proper validation or sanitization. Attackers can exploit this to access unauthorized files or directories, leading to data breaches or other security compromises.
## Remediations
- **Do not** directly use user input in file paths without sanitization. This prevents attackers from manipulating file paths to access or manipulate unauthorized files.
- **Do** use a safelist to define accessible paths or directories. Only allow user input to influence file paths within these predefined, safe boundaries.
- **Do** sanitize user input used in file path resolution. For example, use absolute paths and check against the expected base directory
```python
BASE_DIRECTORY = '/path/to/safe/directory'
my_path = os.path.abspath(os.path.join(BASE_DIRECTORY, user_input))
if my_path.startswith(BASE_DIRECTORY):
open(my_path)
```
cwe_id:
- 73
id: python_lang_path_using_user_input
documentation_url: https://docs.bearer.com/reference/rules/python_lang_path_using_user_input
20 changes: 20 additions & 0 deletions tests/python/django/path_using_user_input/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const {
createNewInvoker,
getEnvironment,
} = require("../../../helper.js")
const { ruleId, ruleFile, testBase } = getEnvironment(__dirname)

describe(ruleId, () => {
const invoke = createNewInvoker(ruleId, ruleFile, testBase)

test("path_using_user_input", () => {
const testCase = "main.py"

const results = invoke(testCase)

expect(results).toEqual({
Missing: [],
Extra: []
})
})
})
10 changes: 10 additions & 0 deletions tests/python/django/path_using_user_input/testdata/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from django.core.files.storage import FileSystemStorage as FSS

# bearer:expected python_django_path_using_user_input
fs = FSS(form.cleaned_data["storage_path"])
request_file = request.FILES['document']
file = fs.save(request_file)

from django.core.files.storage import default_storage
# bearer:expected python_django_path_using_user_input
default_storage.save(form.cleaned_data["filepath"])
20 changes: 20 additions & 0 deletions tests/python/lang/path_using_user_input/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const {
createNewInvoker,
getEnvironment,
} = require("../../../helper.js")
const { ruleId, ruleFile, testBase } = getEnvironment(__dirname)

describe(ruleId, () => {
const invoke = createNewInvoker(ruleId, ruleFile, testBase)

test("path_using_user_input", () => {
const testCase = "main.py"

const results = invoke(testCase)

expect(results).toEqual({
Missing: [],
Extra: []
})
})
})
25 changes: 25 additions & 0 deletions tests/python/lang/path_using_user_input/testdata/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import os
user_upload_path = form.cleaned_data["filepath"]
# bearer:expected python_lang_path_using_user_input
os.mkdir(user_upload_path)

print("What file would you like to read?")
filepath = input()
# bearer:expected python_lang_path_using_user_input
open(filepath)

import fileinput as fi
# bearer:expected python_lang_path_using_user_input
with fi.input(files=(filepath), encoding="utf-8") as f:
for line in f:
process(line)

from pathlib import Path
my_path = Path(filepath)
# bearer:expected python_lang_path_using_user_input
my_path.open()

my_other_path = Path("some/known/path")
# bearer:expected python_lang_path_using_user_input
my_other_path.joinpath(filepath)

0 comments on commit 16f2fae

Please sign in to comment.