Back to skill

Security audit

zsquadbot

Security checks for vulnerabilities and agentic risk

Overview

The skill matches quadruped robot development, but it can activate real robot motors with weak safety controls and includes an unsafe test script that runs code from a local path.

Review carefully before installing or running on real hardware. Use the simulator first, do not connect a physical robot unless it is secured with an emergency stop available, and fix the motor-control code to require explicit arming, validated limits, telemetry health checks, and a real torque-disable stop. Do not run simple_test.py unchanged; replace its hardcoded exec path with a normal package-relative import.

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 (4)

T09 · Insecure Skill Coding Practices

Warning
Location
simple_test.py:4
Finding
Arbitrary Python Execution from an Unverified External File<![CDATA[ ## Vulnerability Details **File Location**: `simple_test.py:4-7` **Vulnerability Type**: Execution of unverified external Python source **Risk Level**: Medium ### Vulnerable Code ```python import time # Directly read and execute sim_state.py exec(open('/Users/liuxing/.openclaw/workspace/skills/quadruped/scripts/sim_state.py').read()) ``` ### Technical Analysis The script reads Python source from a hardcoded absolute path outside the audited project and passes the resulting string directly to `exec()`. The external file is not authenticated, integrity-checked, or guaranteed to be the bundled `scripts/sim_state.py` reviewed during this audit. `exec()` runs the file with the privileges and execution context of the current Python process. Consequently, anyone able to create or modify the referenced file can convert a routine test invocation into arbitrary local code execution. The hardcoded developer-specific location also creates module provenance ambiguity: the code executed at runtime can differ from the version distributed in the Skill package. ### Attack Path 1. An attacker obtains write access to `/Users/liuxing/.openclaw/workspace/skills/quadruped/scripts/sim_state.py`, or creates the path in an environment where it does not already exist. 2. The attacker inserts arbitrary Python statements into that file. 3. A user runs `simple_test.py`, believing it executes the simulator included in the audited project. 4. `open()` reads the attacker-controlled source. 5. `exec()` executes that source without validation or isolation. This path requires the attacker to control the referenced local file or its containing directory; no remote retrieval mechanism was identified. ### Impact Assessment Successful exploitation provides arbitrary code execution with the privileges of the user running `simple_test.py`. Within those privileges, malicious code could read or alter accessible files, execute processes, access locally available credentials, or in ...[truncated 150 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove dynamic source execution entirely. 2. Import the bundled simulator through a normal, package-relative import, for example: ```python from scripts.sim_state import QuadrupedSimulator ``` 3. Package `scripts` as a Python package where necessary and use a controlled project root rather than modifying search paths to point to developer-specific directories. 4. Do not use `exec()` to load project modules. 5. If loading external code is an explicit requirement, require a trusted path, verify the file against a pinned cryptographic digest, and execute it in an appropriately isolated process with minimal permissions. 6. Add a test that confirms the imported module resolves inside the installed project directory. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/motor_control.py:136
Finding
Unsafe Automatic Motor Activation and Unrestricted Actuator Commands<![CDATA[ ## Vulnerability Details **File Location**: `scripts/motor_control.py:136-181, 198-232` **Vulnerability Type**: Missing actuator safety validation and fail-safe controls **Risk Level**: High ### Vulnerable Code ```python def connect(self): """Connect to all motors.""" try: self.ser = serial.Serial(self.port, self.baudrate, timeout=0.1) time.sleep(0.1) # Initialize motors (adjust IDs based on your robot) for motor_id in range(1, 13): motor = Motor(self.port, self.baudrate) motor.connect(motor_id) motor.enable() self.motors[motor_id] = motor print("Robot: Connected and motors enabled") return True except serial.SerialException as e: print(f"Connection error: {e}") return False def emergency_stop(self): """Emergency stop all motors.""" for motor in self.motors.values(): try: motor.set_position(0.0, 0.0, 0.0) print(f"Motor {motor.motor_id}: Emergency stop") except Exception as e: print(f"Motor {motor.motor_id}: Stop error: {e}") ``` ```python elif args.test and args.motor: print(f"Testing motor {args.motor}") robot.connect() # Test position sweep for pos in [0, 45, 90, 45, 0]: robot.get_motor_status(args.motor) robot.motors[args.motor].set_position(pos) time.sleep(1.0) ``` ```python elif cmd.startswith('pos'): pos = float(cmd.split()[1]) robot.motors[mid].set_position(pos) elif cmd.startswith('vel'): vel = float(cmd.split()[1]) robot.motors[mid].set_position(0.0, vel) elif cmd.startswith('force'): force = float(cmd.split()[1]) robot.motors[mid].set_position(0.0, 0.0, force) ``` The unrestricted values are serialized directly in `scripts/motor_control.py:67-84`: ```python def set_position(self, position: float, velocity: float = 0.0, force: float = 0.0): """ Set motor target position. Arg ...[truncated 2913 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Do not enable motors automatically during connection. Separate connection, validation, arming, and enablement into explicit states. 2. Require clear operator confirmation before physical actuator activation. 3. Enable only the motor or motor group explicitly requested by the operation. 4. Check every connection result and abort the complete operation safely if any required motor fails to connect. 5. Define hardware-specific limits for every joint and reject commands outside configured position, velocity, torque or force, acceleration, and temperature limits. 6. Reject `NaN`, positive infinity, and negative infinity using `math.isfinite()`. 7. Apply slew-rate and acceleration limiting before transmitting commands. 8. Require valid, recent telemetry and a healthy error state before enabling or commanding a motor. 9. Implement a communication watchdog that performs a documented hardware torque-disable operation when commands or telemetry time out. 10. Replace the current emergency-stop implementation with the hardware protocol's dedicated disable or torque-off command. Verify acknowledgement and maintain the disabled state until an explicit re-arm sequence occurs. 11. Add a simulation-only default and require an explicit hardware flag for real serial motor control. 12. Add boundary and failure tests using a mocked serial device before performing tests on physical hardware. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/motor_control.py:86
Finding
Incorrect Motor Telemetry Frame Length Causes Safety Monitoring Failure<![CDATA[ ## Vulnerability Details **File Location**: `scripts/motor_control.py:86-113` **Vulnerability Type**: Inconsistent binary packet length validation **Risk Level**: Medium ### Vulnerable Code ```python def get_status(self) -> Dict[str, float]: """ Read motor status and telemetry. Returns: Dictionary with position, velocity, temperature, voltage, and errors """ if not self.ser: raise RuntimeError("Motor not connected") # Request status status_cmd = struct.pack('!BB', 0xFF, self.motor_id) self.ser.write(status_cmd) time.sleep(0.05) # Read response (12 bytes) if self.ser.in_waiting >= 12: data = self.ser.read(12) unpacked = struct.unpack('!fffiiii', data) return { 'position': unpacked[0], # degrees 'velocity': unpacked[1], # rad/s 'force': unpacked[2], # N 'temperature': unpacked[3] / 10.0, # °C (scaled) 'voltage': unpacked[4] / 1000.0, # V 'error_flags': unpacked[5:7] # error codes } return {} ``` ### Technical Analysis The code reads 12 bytes but attempts to unpack the data using `!fffiiii`. This format consists of three four-byte floats and four four-byte integers, requiring 28 bytes in total. Whenever at least 12 bytes are available, the function reads exactly 12 bytes and passes them to a format requiring 28 bytes. Python's `struct.unpack()` therefore raises an exception rather than returning telemetry. There is no local exception handling, frame synchronization, checksum validation, or safe fallback that disables motors when telemetry becomes unavailable. The caller may consequently lose access to position, temperature, voltage, and error-flag information while actuators remain enabled. ### Attack Path 1. The robot is connected and its motors are enabled. 2. The application calls `get_status()` to monitor motor health. 3. The serial device provides at l ...[truncated 878 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Define a single protocol constant for the expected motor status frame size. 2. If `!fffiiii` is the correct format, require and read exactly `struct.calcsize('!fffiiii')`, which is 28 bytes. 3. Use a bounded `read_exactly` loop with a deadline rather than relying only on `in_waiting`. 4. Validate headers, motor identifiers, payload length, checksum or CRC, and packet type before unpacking. 5. Catch `struct.error`, serial exceptions, and timeouts. 6. Treat missing or invalid safety telemetry as a fail-safe event: stop issuing motion commands and send the documented torque-disable command. 7. Add unit tests for complete, partial, oversized, corrupted, and delayed frames. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/imu_reader.py:70
Finding
IMU Parser Reads a Shorter Frame Than It Subsequently Accesses<![CDATA[ ## Vulnerability Details **File Location**: `scripts/imu_reader.py:70-106` **Vulnerability Type**: Out-of-range binary packet parsing and incomplete input validation **Risk Level**: Medium ### Vulnerable Code ```python # Read response (43 bytes) if self.ser.in_waiting >= 43: data = self.ser.read(43) # Parse headers header1, header2, packet_type = struct.unpack('!BBB', data[:3]) # Check for valid IMU packet if header1 != 0x55 or header2 != 0xAA: return None # Unpack data fields accel_x, accel_y, accel_z = struct.unpack('!fff', data[3:12]) gyro_x, gyro_y, gyro_z = struct.unpack('!fff', data[12:21]) quat_x, quat_y, quat_z, quat_w = struct.unpack('!ffff', data[21:37]) temperature = struct.unpack('!f', data[37:41])[0] timestamp = struct.unpack('!I', data[41:45])[0] checksum = struct.unpack('!B', data[45:46])[0] # Validate checksum (simple XOR) calculated_checksum = 0 for i in range(45): calculated_checksum ^= data[i] if checksum != calculated_checksum: print("Warning: IMU checksum mismatch") return None ``` ### Technical Analysis The parser waits for and reads 43 bytes, but its field layout accesses data through byte offset 46: - The timestamp slice `data[41:45]` requires bytes through offset 44. - The checksum slice `data[45:46]` requires byte 45. - The checksum loop indexes `data[0]` through `data[44]`. A 43-byte buffer cannot satisfy these accesses. The timestamp unpack operation first receives only two bytes from `data[41:45]` and raises `struct.error`; if execution reached the checksum loop, indexing beyond the buffer would also fail. Although two header bytes are checked, `packet_type` is parsed and never validated. There is no frame resynchronization or fail-safe response when IMU data cannot be authenticated and decoded. ### Attack Path 1. The IMU monitoring process sends a data request. 2. The serial interface makes at least 43 bytes ava ...[truncated 1049 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Reconcile the protocol specification and implementation. The displayed layout requires 46 bytes, not 43. 2. Calculate the required frame size from explicit field formats rather than duplicating numeric constants. 3. Read the complete frame with a deadline-aware exact-read function. 4. Validate both header bytes, `packet_type`, total length, and checksum before returning sensor values. 5. Catch malformed-frame and serial exceptions without terminating the monitor unexpectedly. 6. Resynchronize to the next valid header after a corrupt or partial frame. 7. Mark IMU data stale after a strict timeout and require active control code to enter a documented safe state. 8. Add tests covering truncated frames at every field boundary, invalid packet types, bad checksums, noise before the header, and delayed serial reads. ]]>
Vulnerability Patterns
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (21)

exec() call detected

High
Category
Dangerous Code Execution
Content
import time

# 直接读取并执行 sim_state.py
exec(open('/Users/liuxing/.openclaw/workspace/skills/quadruped/scripts/sim_state.py').read())

sim = QuadrupedSimulator()
sim.dt = 0.01
Confidence
99% confidence
Finding
The script reads Python source from a filesystem path and executes it with exec(), which allows arbitrary code execution if that file is modified, replaced, or sourced from an untrusted workspace. In a skill environment, this is especially dangerous because the path points into a user-writable skills/workspace directory rather than a trusted packaged module.

Direct flow: open (file read) → exec (code execution)

High
Category
Data Flow
Content
import time

# 直接读取并执行 sim_state.py
exec(open('/Users/liuxing/.openclaw/workspace/skills/quadruped/scripts/sim_state.py').read())

sim = QuadrupedSimulator()
sim.dt = 0.01
Confidence
99% confidence
Finding
This is a direct taint-style flow from file read to code execution: open(...).read() feeds directly into exec(...), creating an immediate arbitrary-code-execution sink. Any attacker who can influence the referenced file or its path gains execution in the current process with the script's privileges.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The motor-control examples show direct actuator enablement and position commands against a real hardware interface without an immediate, explicit warning that running the snippet can move physical joints. In a quadruped robotics skill, that omission is safety-relevant because users may copy and run the code on connected hardware, causing unintended motion, pinching, tip-over, or equipment damage if the robot is not secured or tested in simulation first.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The CLI examples for calibration and motor testing present commands that may change motor state or trigger movement, but they do not warn that these operations can affect physical hardware. Because these are copy-pasteable commands in a robotics skill, users may execute them on a live robot without securing the platform or understanding the consequences, leading to unsafe actuation or altered calibration state.

Natural-Language Policy Violations

Medium
Confidence
96% confidence
Finding
The user-facing comments and echoed output throughout the script are entirely in Chinese, which imposes a specific language on users. Under the policy rule, this is a natural-language locale choice that should either be optional, user-selectable, or clearly justified as region-specific.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
This Python file contains multiple user-facing print strings in Chinese, including the main test banner and gait labels. The policy requires avoiding forced language or locale constraints unless the skill offers user choice or clearly documents that it is region-specific, which is not present here.

Intent-Code Divergence

Medium
Confidence
99% confidence
Finding
The code checks for only 43 available bytes and then reads 43 bytes, but immediately parses and indexes the buffer as if it contains 46 bytes. A malformed or normal short packet from the serial device can therefore trigger struct unpack errors or crash the reader, causing a denial of service in any process relying on this IMU input. In a robotics context, loss of sensor ingestion can degrade stability, monitoring, or control decisions.

Natural-Language Policy Violations

Medium
Confidence
96% confidence
Finding
This Python file contains multiple user-facing print strings in Chinese in the main execution path. Because the skill does not offer an opt-in language choice or explain that it is intended only for a Chinese-speaking context, it violates the language/locale policy for natural-language content.

Unbounded Resource Access

Medium
Category
Excessive Agency
Content
True if connection successful
        """
        try:
            self.ser = serial.Serial(self.port, self.baudrate, timeout=0.1)
            self.motor_id = motor_id
            time.sleep(0.1)  # Allow connection to stabilize
            return True
Confidence
75% confidence
Finding
Skill allows unbounded resource consumption (API calls, storage, compute). Without rate limits or quotas, a compromised or misbehaving agent can cause denial-of-service or cost overruns.

Unbounded Resource Access

Medium
Category
Excessive Agency
Content
True if connection successful
        """
        try:
            self.ser = serial.Serial(self.port, self.baudrate, timeout=0.1)
            self.motor_id = motor_id
            time.sleep(0.1)  # Allow connection to stabilize
            return True
Confidence
75% confidence
Finding
Skill allows unbounded resource consumption (API calls, storage, compute). Without rate limits or quotas, a compromised or misbehaving agent can cause denial-of-service or cost overruns.

Unbounded Resource Access

Medium
Category
Excessive Agency
Content
True if connection successful
        """
        try:
            self.ser = serial.Serial(self.port, self.baudrate, timeout=0.1)
            self.motor_id = motor_id
            time.sleep(0.1)  # Allow connection to stabilize
            return True
Confidence
75% confidence
Finding
Skill allows unbounded resource consumption (API calls, storage, compute). Without rate limits or quotas, a compromised or misbehaving agent can cause denial-of-service or cost overruns.

Intent-Code Divergence

Medium
Confidence
97% confidence
Finding
The status parser is internally inconsistent: the code checks for 12 bytes but then attempts to unpack the buffer with the format '!fffiiii', which requires 28 bytes. This can cause exceptions, misparsed telemetry, or unsafe control decisions based on invalid status data, which is particularly risky in a robot motor-control context where feedback integrity matters.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The connect path automatically iterates through all 12 motors, connects to each, and immediately enables them without any explicit safety interlock, operator confirmation, or warning about physical motion risk. In a quadruped robot context, unexpected enablement can result in sudden actuation, pinching, falls, or equipment damage even if the code is not malicious.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The CLI test and interactive modes issue live position, velocity, and force commands directly to motors, but provide no clear safety disclosure, environment checks, or operational guardrails. Because this skill controls real actuators, a user can trigger hazardous movement during testing or interactive use without being prompted to secure the robot or confirm intent.

Natural-Language Policy Violations

Medium
Confidence
97% confidence
Finding
The monitor banner at L098 uses Chinese text in a user-facing interface, and similar Chinese-only status text appears elsewhere in the file. The skill does not offer localization, language selection, or any justification that it is intended only for a Chinese-speaking or region-specific context, which makes this a natural-language locale policy issue.

Natural-Language Policy Violations

Medium
Confidence
96% confidence
Finding
Lines L163, L187, and L189 emit Chinese-only runtime messages to the user. Because the file provides no mechanism for selecting language and no documented reason for a fixed Chinese locale, this violates the rule against forcing a specific language without user opt-in.

Natural-Language Policy Violations

Medium
Confidence
92% confidence
Finding
The file’s human-facing text is entirely in Chinese, including the module docstring and all printed output. This imposes a specific language/locale on users without offering any choice or documenting a justified locale restriction, which matches the language-policy violation criteria.

Natural-Language Policy Violations

Medium
Confidence
94% confidence
Finding
This shell script includes user-facing natural-language text exclusively in Chinese, including status messages and usage guidance. For an organizational language/locale policy, forcing a specific language without user opt-in can violate the requirement to offer language choice or clearly justify the locale restriction.

Natural-Language Policy Violations

Low
Confidence
94% confidence
Finding
The docstring and all printed user-facing strings are in Chinese, which imposes a specific language on users. The file does not provide any opt-in, fallback, or explanation that this is intentionally limited to a Chinese-speaking context.

Intent-Code Divergence

Low
Confidence
88% confidence
Finding
The method documentation presents duration as part of creating a complete gait profile, implying the output should reflect the requested time span. In implementation, duration is never used, so the function does not actually create a duration-specific profile and instead just delegates to gait generators and returns their raw output with a frequency value.

Intent-Code Divergence

Low
Confidence
95% confidence
Finding
The monitor UI documents 'S - Stretch pose' in the controls, implying uppercase S is a valid command. However, input is normalized with `.lower()` before dispatch, so uppercase 'S' becomes 's' and triggers `set_static_pose()` instead; `set_stretch_pose()` is therefore unreachable via the documented command path.

Static analysis

No suspicious patterns detected.