Skip to content

Commit

Permalink
feat(python): path traversal CWE-22 (#403)
Browse files Browse the repository at this point in the history
  • Loading branch information
elsapet authored May 16, 2024
1 parent 90d490d commit 083f8e5
Show file tree
Hide file tree
Showing 6 changed files with 404 additions and 0 deletions.
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

0 comments on commit 083f8e5

Please sign in to comment.