Skip to content
Open
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
45 changes: 45 additions & 0 deletions bandit/plugins/general_bad_file_permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,23 @@
.. versionchanged:: 1.7.5
Added checks for S_IWGRP and S_IXOTH

.. versionchanged:: 1.9.5
Added detection of stat module constants (e.g., stat.S_IWOTH)

""" # noqa: E501
import ast
import stat

import bandit
from bandit.core import issue
from bandit.core import test_properties as test

# Mapping of stat module constant names to their integer values.
# Only includes flags that are relevant to file permission checks.
_STAT_CONSTANTS = {
name: getattr(stat, name) for name in dir(stat) if name.startswith("S_I")
Comment on lines +67 to +69
}


def _stat_is_dangerous(mode):
return (
Expand All @@ -69,13 +79,48 @@ def _stat_is_dangerous(mode):
)


def _resolve_stat_expression(node):
"""Resolve an AST node to an integer value if it's a stat constant
expression (e.g., stat.S_IWOTH | stat.S_IWGRP).

Returns the integer value if resolvable, or None if not.
"""
if isinstance(node, ast.Constant) and isinstance(node.value, int):
return node.value

if isinstance(node, ast.Attribute):
# Handle stat.S_IWOTH, stat.S_IRWXU, etc.
if (
isinstance(node.value, ast.Name)
and node.value.id == "stat"
and node.attr in _STAT_CONSTANTS
):
return _STAT_CONSTANTS[node.attr]

if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr):
left = _resolve_stat_expression(node.left)
right = _resolve_stat_expression(node.right)
if left is not None and right is not None:
return left | right

return None


@test.checks("Call")
@test.test_id("B103")
def set_bad_file_permissions(context):
if "chmod" in context.call_function_name:
if context.call_args_count == 2:
mode = context.get_call_arg_at_position(1)

# Try to resolve stat constant expressions from the raw AST node
if mode is None or not isinstance(mode, int):
raw_args = context._context["call"].args
if len(raw_args) >= 2:
resolved = _resolve_stat_expression(raw_args[1])
if resolved is not None:
mode = resolved
Comment on lines +118 to +122

if (
mode is not None
and isinstance(mode, int)
Expand Down
1 change: 1 addition & 0 deletions examples/os-chmod.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@
os.chmod(keyfile, 0o777)
os.chmod('~/hidden_exec', stat.S_IXGRP)
os.chmod('~/hidden_exec', stat.S_IXOTH)
os.chmod('config.ini', stat.S_IWGRP | stat.S_IWOTH)
4 changes: 2 additions & 2 deletions tests/functional/test_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,8 +269,8 @@ def test_subdirectory_okay(self):
def test_os_chmod(self):
"""Test setting file permissions."""
expect = {
"SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 4, "HIGH": 8},
"CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 1, "HIGH": 11},
"SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 6, "HIGH": 9},
"CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 1, "HIGH": 14},
}
self.check_example("os-chmod.py", expect)

Expand Down