Back to skill

Security audit

PB Module Triage

Security checks for vulnerabilities and agentic risk

Overview

The skill has a coherent PB-log triage purpose, but it needs review because its helper script can execute Python code from the target project’s protobuf directory and can read very large recordings into memory.

Install only if you will use it on trusted BrainNode repositories and trusted generated protobuf bindings. Do not point --repo-root or --proto-dir at untrusted directories, prefer a sandboxed/runtime-limited environment, and lower --max-file-bytes for large recordings. Expect it to read PB files from the project and optionally write a JSON report when requested.

Vulnerability Patterns
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (2)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/pb_module_triage.py:247
Finding

Arbitrary Code Execution Through Untrusted Protobuf Module Import

Content
View full analysis
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/pb_module_triage.py:990
Finding

Denial of Service Through Whole-File Protobuf Loading

Content
View full analysis
args.max_file_bytes: skipped.append(f"{file_path}: skipped size {human_size(size)} > {human_size(args.max_file_bytes)}") continue total_bytes += size try: raw = file_path.read_bytes() except OSError as exc: skipped.append(f"{file_path}: read failed: {exc}") continue ``` The default threshold is configured separately as follows: ```python parser.add_argument( "--max-file-bytes", type=parse_size, default=parse_size("2g"), help="Skip files larger than this", ) ``` ### Technical Analysis Every accepted protobuf file is loaded into one contiguous `bytes` object through `Path.read_bytes()`. The default per-file limit is 2 GiB, which is large enough to exhaust the memory available to many Agent processes or containers. The raw allocation is not the only memory cost. Subsequent protobuf parsing and construction of decoded frame objects can retain the original bytes while allocating additional objects and payloads. Peak memory consumption may therefore substantially exceed the input file size. The limit is per file rather than cumulative. Up to the configured `--max-files` files may be processed, and decoded frame data can accumulate in `frames_by_module`. The script records `total_bytes`, but does not enforce a maximum cumulative byte or decoded-frame budget. ### Attack Path 1. An attacker supplies, or causes the skill to discover, one or more large `.pb` files. 2. Each file remains below the default 2 GiB per-file threshold. 3. The recursive discovery process selects the files for analysis. 4. `read_bytes()` attempts to allocate memory for the complete content of each file. 5. Protobuf decoding creates additional in-memory messages and frame references. 6. The process exceeds its memory limit, is t ...[truncated 786 chars]
Remediation
View remediation
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (12)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding

The skill instructs the agent to read project files, locate PB recordings, and optionally write JSON reports, but it does not declare any explicit tool scope such as allowed tools or permissions. In practice this can let a host agent grant broader-than-necessary file access, increasing the risk of unintended reading of unrelated files or writing outputs in unsafe locations when handling adversarial paths or large repositories.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

This instruction imposes a default language choice in the skill's output contract rather than preserving user locale unless they opt in. That can violate language/locale policy because it forces a specific language by default for all users, regardless of their preferred language.

Content

No source excerpt is available for this finding.

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Confidence
50% confidence
Finding

Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Content

Scanner excerpt · scripts/pb_module_triage.py (reported line 190)May include surrounding context.

python
if stamp > 0:
        return stamp
    for name in ("point_cloud", "image", "wifi_data"):
        child = getattr(message, name, None)
        if child is not None:
            stamp = stamp_ns_from_header(getattr(child, "header", None))
            if stamp > 0:

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Confidence
50% confidence
Finding

Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Content

Scanner excerpt · scripts/pb_module_triage.py (reported line 196)May include surrounding context.

python
if stamp > 0:
                return stamp
    for name in ("gps", "imu", "h264", "rangeframe"):
        items = getattr(message, name, [])
        if items:
            stamp = stamp_ns_from_header(getattr(items[0], "header", None))
            if stamp > 0:

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Confidence
50% confidence
Finding

Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Content

Scanner excerpt · scripts/pb_module_triage.py (reported line 306)May include surrounding context.

python
count = 0
    for field_name, _, _, _ in BUFFER_SPECS:
        if hasattr(total, field_name):
            count += len(getattr(total, field_name))
    return count

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Confidence
50% confidence
Finding

Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Content

Scanner excerpt · scripts/pb_module_triage.py (reported line 358)May include surrounding context.

python
for type_name, module, label in direct_message_candidates(pb2):
        if not hasattr(pb2, type_name):
            continue
        message = getattr(pb2, type_name)()
        try:
            message.ParseFromString(raw)
        except DecodeError:

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Confidence
50% confidence
Finding

Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Content

Scanner excerpt · scripts/pb_module_triage.py (reported line 385)May include surrounding context.

python
for field_name, type_name, module, _ in BUFFER_SPECS:
            if not hasattr(total, field_name) or not hasattr(pb2, type_name):
                continue
            message_cls = getattr(pb2, type_name)
            for payload in getattr(total, field_name):
                message = message_cls()
                try:

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Confidence
50% confidence
Finding

Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Content

Scanner excerpt · scripts/pb_module_triage.py (reported line 386)May include surrounding context.

python
if not hasattr(total, field_name) or not hasattr(pb2, type_name):
                continue
            message_cls = getattr(pb2, type_name)
            for payload in getattr(total, field_name):
                message = message_cls()
                try:
                    message.ParseFromString(payload)

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Confidence
50% confidence
Finding

Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Content

Scanner excerpt · scripts/pb_module_triage.py (reported line 689)May include surrounding context.

python
if getattr(frame, "task_id", ""):
            task_ids[getattr(frame, "task_id")] += 1
        for name, counter in modes.items():
            counter[enum_name(frame, name, int(getattr(frame, name, 0)))] += 1
        for name, counter in states.items():
            counter[enum_name(frame, name, int(getattr(frame, name, 0)))] += 1
    summary.metrics["modes"] = {name: counter_to_dict(counter) for name, counter in modes.items()}

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Confidence
50% confidence
Finding

Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Content

Scanner excerpt · scripts/pb_module_triage.py (reported line 691)May include surrounding context.

python
if getattr(frame, "task_id", ""):
            task_ids[getattr(frame, "task_id")] += 1
        for name, counter in modes.items():
            counter[enum_name(frame, name, int(getattr(frame, name, 0)))] += 1
        for name, counter in states.items():
            counter[enum_name(frame, name, int(getattr(frame, name, 0)))] += 1
    summary.metrics["modes"] = {name: counter_to_dict(counter) for name, counter in modes.items()}

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Confidence
50% confidence
Finding

Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Content

Scanner excerpt · scripts/pb_module_triage.py (reported line 721)May include surrounding context.

python
for field_name in status_fields:
            if not has_field(frame, field_name):
                continue
            status = getattr(frame, field_name)
            level = int(getattr(status, "level", 0))
            name = getattr(status, "name", "") or field_name
            names[name] += 1

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
80% confidence
Finding

This code file performs a file write when --output is provided, but there is no confirmation prompt, visible user-facing log message, or explanatory comment/docstring near the write. For a safety review under this rule, file writes should include some form of disclosure unless clearly warned elsewhere.

Content

No source excerpt is available for this finding.