diff --git a/bandit/plugins/general_bad_file_permissions.py b/bandit/plugins/general_bad_file_permissions.py index 7d3fce4df..b940604b0 100644 --- a/bandit/plugins/general_bad_file_permissions.py +++ b/bandit/plugins/general_bad_file_permissions.py @@ -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") +} + def _stat_is_dangerous(mode): return ( @@ -69,6 +79,33 @@ 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): @@ -76,6 +113,14 @@ def set_bad_file_permissions(context): 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 + if ( mode is not None and isinstance(mode, int) diff --git a/examples/os-chmod.py b/examples/os-chmod.py index f7fff8517..330cf96b9 100644 --- a/examples/os-chmod.py +++ b/examples/os-chmod.py @@ -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) diff --git a/tests/functional/test_functional.py b/tests/functional/test_functional.py index 08b1c5c5b..b0a18a35d 100644 --- a/tests/functional/test_functional.py +++ b/tests/functional/test_functional.py @@ -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)