Skip to content

Python: .NET: [Bug]: _CodeValidator does not enforce the documented os.environ/os.path-only policy for aliased imports #7068

Description

@AAtomical

Description

The documented contract

The validator documents and intends an os-restricted-to-environ/path policy, in three places in its own source:

  • ALLOWED_IMPORTS admits os explicitly scoped:
    "os",  # Limited to os.environ, os.path - validated via attribute access
  • The attribute allow-list:
    ALLOWED_OS_ATTRS: set[str] = {"environ", "path"}
  • visit_Attribute’s own comment states the contract:

    Enforce the os attribute allow-list. Anything outside ALLOWED_OS_ATTRS (file I/O, process control, mutating helpers, etc.) is rejected so the validator matches the documented os.environ / os.path-only contract.

So the intended, documented behaviour is unambiguous: through os, only environ and path should be reachable.

The inconsistency

Enforcement is keyed on the literal receiver name os. In visit_Attribute:

if isinstance(node.value, ast.Name) and node.value.id == "os" and node.attr not in self._allowed_os_attrs:
    self._errors.append(f"Access to os.{node.attr} is not allowed")

visit_Import admits the module but never records its bound name (asname), and assignments aren’t tracked. So any name other than os bound to the os module escapes the check entirely, even though the accessed attribute (system, popen, execvp, fork, unlink, chmod, …) is identical.

Notably, visit_ImportFrom already enforces this same policy for the from os import system form:

elif module_name == "os":
    for alias_node in node.names:
        if alias_node.name not in self._allowed_os_attrs:
            self._errors.append(f"Import from 'os' of '{alias_node.name}' is not allowed")

Code Sample

#### Reproduction

Standalone against `validator.py`, same commit:


# (a) direct — policy enforced ✔
$ echo '{"code":"import os\nos.system(\"id\")"}' | python -I validator.py
{"errors": ["Access to os.system is not allowed"]}          exit=1

# (b) from-import — policy enforced ✔  (existing visit_ImportFrom guard)
$ echo '{"code":"from os import system\nsystem(\"id\")"}' | python -I validator.py
{"errors": ["Import from 'os' of 'system' is not allowed"]} exit=1

# (c) module alias — SAME os.system, NOT enforced (X)
$ echo '{"code":"import os as x\nx.system(\"id\")"}' | python -I validator.py
(no errors)                                                 exit=0

# (d) assignment alias — NOT enforced ✗
$ echo '{"code":"import os\n_o = os\n_o.system(\"id\")"}' | python -I validator.py
(no errors)                                                 exit=0

Error Messages / Stack Traces

Package Versions

Microsoft.Agents.AI.LocalCodeAct: source build @ main 68136ee

.NET Version

.NET 8.0

Additional Context

Suggested fix

Track the names bound to os and check membership instead of a literal == "os" — mirroring the enforcement already present in visit_ImportFrom:

def __init__(self, ...):
    ...
    self._os_aliases: set[str] = {"os"}

def visit_Import(self, node: ast.Import) -> None:
    for alias in node.names:
        if alias.name.split(".")[0] == "os":
            self._os_aliases.add(alias.asname or "os")
    self.generic_visit(node)

def visit_Assign(self, node: ast.Assign) -> None:
    if (isinstance(node.value, ast.Name) and node.value.id in self._os_aliases
            and len(node.targets) == 1 and isinstance(node.targets[0], ast.Name)):
        self._os_aliases.add(node.targets[0].id)
    self.generic_visit(node)

def visit_Attribute(self, node: ast.Attribute) -> None:
    if (isinstance(node.value, ast.Name) and node.value.id in self._os_aliases
            and node.attr not in self._allowed_os_attrs):
        self._errors.append(f"Access to os.{node.attr} is not allowed")
    ...

Metadata

Metadata

Assignees

No one assigned

    Labels

    .NETUsage: [Issues, PRs], Target: .Netneeds-maintainer-triageUsage: [Issues], Target: all issues that the automated triage flow failed to processpythonUsage: [Issues, PRs], Target: PythontriageUsage: [Issues], Target: All issues that still need to be triaged

    Type

    Fields

    No fields configured for Bug.

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions