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

feat(python): path traversal CWE-22 #403

Merged
merged 2 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions rules/python/django/path_traversal.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
imports:
- python_shared_lang_dynamic_input
- python_shared_lang_import2
- python_shared_lang_import4
patterns:
- pattern: $<FILE_SYSTEM_STORAGE>($<...>$<UNSANITIZED_DYNAMIC_INPUT>$<...>)
filters:
- variable: UNSANITIZED_DYNAMIC_INPUT
detection: python_django_path_traversal_dynamic_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($<UNSANITIZED_DYNAMIC_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: UNSANITIZED_DYNAMIC_INPUT
detection: python_django_path_traversal_dynamic_input
scope: result
auxiliary:
- id: python_django_path_traversal_dynamic_input
sanitizer: python_django_path_traversal_sanitizer
patterns:
- pattern: $<UNSANITIZED_DYNAMIC_INPUT>
filters:
- variable: UNSANITIZED_DYNAMIC_INPUT
detection: python_shared_lang_dynamic_input
scope: cursor
- id: python_django_path_traversal_sanitizer
patterns:
- pattern: $<OS_PATH_NORMPATH>()
filters:
- variable: OS_PATH_NORMPATH
detection: python_shared_lang_import2
scope: result
filters:
- variable: MODULE1
values: [os]
- variable: MODULE2
values: [path]
- variable: NAME
values: [normpath]
languages:
- python
severity: high
metadata:
description: Unsanitized dynamic input in file path
remediation_message: |
## Description

Using unsanitized dynamic input to determine file paths can allow attackers to gain access to files and folders outside of the intended scope. This vulnerability occurs when input provided by users is directly used to access the filesystem without proper validation or sanitization.

## Remediations

- **Do not** directly use external input to construct file paths. This can lead to unauthorized file access.
- **Do** sanitize external input used in file paths. Use `os.path.normpath` to normalize paths and remove any redundant separators in order to prevent path traversal attacks.
```python
os.path.normpath(os.path.join(base_directory, user_input))
```
- **Do** use absolute path checks to confirm that the constructed path is within the expected directory
```python
base = os.path.abspath(base_directory)
user_path = os.path.abspath(os.path.join(base_directory, user_input))
if user_path.startswith(base)
# Handle or reject the input
```

## References

- [OWASP Path Traversal](https://owasp.org/www-community/attacks/Path_Traversal)
cwe_id:
- 22
id: python_django_path_traversal
documentation_url: https://docs.bearer.com/reference/rules/python_django_path_traversal
241 changes: 241 additions & 0 deletions rules/python/lang/path_traversal.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
imports:
- python_shared_lang_dynamic_input
- python_shared_lang_import1
- python_shared_lang_import2
patterns:
- pattern: open($<UNSANITIZED_DYNAMIC_INPUT>$<...>)
filters:
- variable: UNSANITIZED_DYNAMIC_INPUT
detection: python_lang_path_traversal_dynamic_input
scope: result
- pattern: $<FILEINPUT>($<...>files=$<UNSANITIZED_DYNAMIC_INPUT>$<...>)
filters:
- variable: FILEINPUT
detection: python_shared_lang_import1
scope: cursor
filters:
- variable: MODULE1
values: [fileinput]
- variable: NAME
values:
- input
- FileInput
- variable: UNSANITIZED_DYNAMIC_INPUT
detection: python_lang_path_traversal_dynamic_input
scope: result
- pattern: $<IO>($<UNSANITIZED_DYNAMIC_INPUT>$<...>)
filters:
- variable: UNSANITIZED_DYNAMIC_INPUT
detection: python_lang_path_traversal_dynamic_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>($<UNSANITIZED_DYNAMIC_INPUT>)
filters:
- variable: UNSANITIZED_DYNAMIC_INPUT
detection: python_lang_path_traversal_dynamic_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>($<UNSANITIZED_DYNAMIC_INPUT>)
filters:
- variable: UNSANITIZED_DYNAMIC_INPUT
detection: python_lang_path_traversal_dynamic_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_lang_path_traversal_dynamic_input
scope: result
- variable: DEST
detection: python_lang_path_traversal_dynamic_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>($<UNSANITIZED_DYNAMIC_INPUT>$<...>)
filters:
- variable: UNSANITIZED_DYNAMIC_INPUT
detection: python_lang_path_traversal_dynamic_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_traversal_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>($<UNSANITIZED_DYNAMIC_INPUT>)
filters:
- variable: UNSANITIZED_DYNAMIC_INPUT
detection: python_lang_path_traversal_dynamic_input
scope: result
- variable: PATH
detection: python_lang_path_traversal_path_module_init_without_user_input
scope: result
- variable: METHOD
values:
- joinpath
- rename
- replace
- symlink_to
- touch
auxiliary:
- id: python_lang_path_traversal_dynamic_input
sanitizer: python_lang_path_traversal_sanitizer
patterns:
- pattern: $<UNSANITIZED_DYNAMIC_INPUT>
filters:
- variable: UNSANITIZED_DYNAMIC_INPUT
detection: python_shared_lang_dynamic_input
scope: result
- id: python_lang_path_traversal_sanitizer
patterns:
- pattern: $<OS_PATH_NORMPATH>()
filters:
- variable: OS_PATH_NORMPATH
detection: python_shared_lang_import2
scope: result
filters:
- variable: MODULE1
values: [os]
- variable: MODULE2
values: [path]
- variable: NAME
values: [normpath]
- id: python_lang_path_traversal_path_module_init_without_user_input
patterns:
- pattern: $<PATH_MODULE>
filters:
- variable: PATH_MODULE
detection: python_lang_path_traversal_path_module
- pattern: $<PATH_MODULE>($<...>)
filters:
- variable: PATH_MODULE
detection: python_lang_path_traversal_path_module
- id: python_lang_path_traversal_path_module_init_with_user_input
patterns:
- pattern: $<PATH_MODULE>($<UNSANITIZED_DYNAMIC_INPUT>)
filters:
- variable: PATH_MODULE
detection: python_lang_path_traversal_path_module
- variable: UNSANITIZED_DYNAMIC_INPUT
detection: python_lang_path_traversal_dynamic_input
scope: result
- id: python_lang_path_traversal_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 dynamic input in file path
remediation_message: |
## Description

Using unsanitized dynamic input to determine file paths can allow attackers to gain access to files and folders outside of the intended scope. This vulnerability occurs when input provided by users is directly used to access the filesystem without proper validation or sanitization.

## Remediations

- **Do not** directly use external input to construct file paths. This can lead to unauthorized file access.
- **Do** sanitize external input used in file paths. Use `os.path.normpath` to normalize paths and remove any redundant separators in order to prevent path traversal attacks.
```python
os.path.normpath(os.path.join(base_directory, user_input))
```
- **Do** use absolute path checks to confirm that the constructed path is within the expected directory
```python
base = os.path.abspath(base_directory)
user_path = os.path.abspath(os.path.join(base_directory, user_input))
if user_path.startswith(base)
# Handle or reject the input
```

## References

- [OWASP Path Traversal](https://owasp.org/www-community/attacks/Path_Traversal)
cwe_id:
- 22
id: python_lang_path_traversal
documentation_url: https://docs.bearer.com/reference/rules/python_lang_path_traversal
20 changes: 20 additions & 0 deletions tests/python/django/path_traversal/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_traversal", () => {
const testCase = "main.py"

const results = invoke(testCase)

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

# bearer:expected python_django_path_traversal
fs = FSS(sys.argv[0])
request_file = request.FILES['document']
file = fs.save(request_file)

from django.core.files.storage import default_storage
import os.path

default_storage.save(os.path.normpath(sys.argv[1]))
x = os.path.normpath(sys.argv[2])
default_storage.save(x)
20 changes: 20 additions & 0 deletions tests/python/lang/path_traversal/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_traversal", () => {
const testCase = "main.py"

const results = invoke(testCase)

expect(results).toEqual({
Missing: [],
Extra: []
})
})
})
Loading
Loading