Back to skill

Security audit

Mikrotik RouterOS

Security checks for vulnerabilities and agentic risk

Overview

This router-management skill is purpose-aligned, but it handles powerful router credentials and commands with under-scoped safeguards.

Review before installing. Use only with dedicated least-privileged RouterOS accounts, avoid admin or empty passwords, do not store router passwords in TOOLS.md or command-line arguments, and avoid using it on untrusted networks until TLS support and certificate validation are actually implemented. Treat custom commands, reboot, shutdown, backup, cleanup, and network scan requests as high-impact actions requiring explicit user approval.

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

Error
Location
client.py:29
Finding

Router Administrator Credentials Are Transmitted Without TLS

Content
View full analysis

Vulnerability Details

File Location: client.py:29-32 and client.py:182-193
Vulnerability Type: Plaintext transmission of sensitive credentials
Risk Level: High

Vulnerable Code

python
def connect(self):
    """Establish connection to RouterOS"""
    try:
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.settimeout(self.timeout)
        self.sock.connect((self.host, self.port))
        self.sock.setblocking(0)  # Non-blocking mode (select handles timeout)
        return True
python
def login(self):
    """Login to RouterOS"""
    if not self.sock:
        return False
        
    try:
        # Send login command
        self._send_word('/login')
        self._send_word(f'=name={self.username}')
        self._send_word(f'=password={self.password}')
        self._send_word('')  # End marker

Technical Analysis

The client always constructs a standard TCP socket. It never wraps that socket with Python's ssl module, creates an SSLContext, validates a server certificate, or performs a TLS handshake.

The username and password are then serialized directly into RouterOS API words and sent over this socket. On the default RouterOS API port 8728, these values are therefore transmitted without transport encryption.

Selecting port 8729 does not make the connection secure because the implementation still uses an ordinary TCP socket. This also conflicts with the SSL support claimed in README.md. A genuine RouterOS API-SSL endpoint will expect a TLS handshake rather than the plaintext API words sent by this implementation.

Attack Path

  1. A user connects to a RouterOS device using the default port 8728.
  2. The client opens a plaintext TCP connection.
  3. The client sends the configured username and password in RouterOS API sentences.
  4. An attacker with access to the same network, a compromised gateway, or another ...[truncated 958 chars]
Remediation
View remediation

Remediation Suggestions

  • Implement RouterOS API-SSL using ssl.create_default_context() and SSLContext.wrap_socket().
  • Validate the server certificate and hostname by default.
  • Allow a private certificate authority to be configured for privately managed routers.
  • Make encrypted transport the default and reject plaintext authentication unless the user explicitly enables an insecure compatibility option.
  • Do not infer TLS solely from port 8729; use an explicit transport-security setting.
  • Clearly warn users when plaintext port 8728 is selected.
  • Update the README so its SSL claims match the actual implementation.
  • Add integration tests that confirm a TLS handshake occurs and invalid or untrusted certificates are rejected.

T09 · Insecure Skill Coding Practices

Warning
Location
SKILL.md:17
Finding

Recommended Configuration Stores Privileged Router Credentials in Plaintext

Content
View full analysis

Vulnerability Details

File Location: SKILL.md:17-26
Vulnerability Type: Insecure plaintext secret storage
Risk Level: Medium

Vulnerable Documentation

markdown
### Option 1: TOOLS.md (Recommended)

Add device info in `~/.openclaw/workspace/TOOLS.md`:

### MikroTik Devices
- **office**: 192.168.88.1, admin, empty password

**Password format:**
- Empty password: write `empty password`, `no password`, `none`, or leave blank
- With password: write the password string directly

Technical Analysis

The primary configuration workflow recommends storing RouterOS usernames and passwords directly in ~/.openclaw/workspace/TOOLS.md. This creates a persistent plaintext copy of a sensitive infrastructure credential inside an Agent workspace.

Although a later section warns users not to store administrator passwords in an unencrypted TOOLS.md, that warning contradicts the earlier recommendation and does not prevent users from following the insecure workflow. The reviewed Python files do not provide secure secret-store integration.

Workspace files may be exposed to other local processes, unrelated skills, Agent context collection, backups, synchronization systems, accidental repository commits, or users with access to the workspace.

Attack Path

  1. A user follows the recommended configuration instructions.
  2. The user writes a RouterOS username and password directly into TOOLS.md.
  3. The plaintext file is read by another local process, skill, user, backup system, or workspace indexing mechanism, or is accidentally committed to source control.
  4. The attacker retrieves the router credentials.
  5. The attacker authenticates to the RouterOS device and exercises the permissions of that account.

Impact Assessment

Exposure grants the attacker the privileges of the stored RouterOS account. If an administrator account is used, the attacker may gain full control over firewall policy, rout ...[truncated 204 chars]

Remediation
View remediation

Remediation Suggestions

  • Remove the recommendation to store passwords directly in TOOLS.md.
  • Integrate with an operating-system keyring, managed secret store, or other credential provider.
  • Keep only a non-secret device alias and host address in workspace configuration.
  • If file-based storage is unavoidable, use a dedicated secret file outside the workspace, enforce restrictive permissions, and clearly document the remaining risk.
  • Never recommend an empty password or a default administrator account.
  • Instruct users to create a dedicated, least-privileged RouterOS account containing only permissions required by the intended operations.
  • Add secret scanning and repository ignore rules for local configuration files.
  • Consolidate the documentation so credential guidance is consistent and does not contradict its own security warnings.

T09 · Insecure Skill Coding Practices

Warning
Location
cli.py:11
Finding

CLI Password Argument Exposes Router Credentials to Local Observation

Content
View full analysis

Vulnerability Details

File Location: cli.py:11-20
Vulnerability Type: Sensitive data exposure through process arguments
Risk Level: Medium

Vulnerable Code

python
def main():
    parser = argparse.ArgumentParser(description='MikroTik RouterOS API CLI Tool')
    parser.add_argument('host', help='RouterOS device IP address')
    parser.add_argument('-u', '--username', default='admin', help='Username (default: admin)')
    parser.add_argument('-p', '--password', default='', help='Password (default: empty)')
    parser.add_argument('--port', type=int, default=8728, help='API port (default: 8728)')
    parser.add_argument('command', choices=['status', 'firewall', 'interfaces', 'routes', 'cmd'], 
                        help='Command to execute')
    parser.add_argument('args', nargs='*', help='Command arguments')

    args = parser.parse_args()

Technical Analysis

The CLI accepts a router password through the --password or -p command-line option. Command-line arguments can be exposed through:

  • Process inspection tools
  • Operating-system process metadata
  • Shell history
  • Terminal session recording
  • Audit and monitoring systems
  • CI/CD and automation logs
  • Wrapper scripts and diagnostic output

The README claims support for MIKROTIK_USER and MIKROTIK_PASS, but cli.py does not read either environment variable. Consequently, users following the functional CLI path are encouraged to place the password directly in the process argument vector.

Attack Path

  1. A user runs the CLI with -p or --password.
  2. The router password becomes part of the command line and process argument vector.
  3. A local user, monitoring agent, process collector, shell-history reader, or log consumer records or observes the argument.
  4. The attacker recovers the plaintext password.
  5. The attacker authenticates to the router using the exposed credentials.

Impact Assessment

...[truncated 402 chars]

Remediation
View remediation

Remediation Suggestions

  • Remove or deprecate the plaintext --password option.
  • Prompt interactively with getpass.getpass() when no secure credential source is configured.
  • Support an operating-system keyring or managed secret provider.
  • If environment-variable support is retained, implement it correctly and document that environment variables may also be visible in some environments.
  • Permit reading the secret from a protected file descriptor or restricted secret file for automation.
  • Remove examples that place passwords directly on the command line.
  • Ensure errors and debug logs never print passwords or complete connection objects containing credentials.

T09 · Insecure Skill Coding Practices

Warning
Location
client.py:182
Finding

Login Logic Treats Authentication Errors and Timeouts as Success

Content
View full analysis

Vulnerability Details

File Location: client.py:182-207
Vulnerability Type: Improper authentication-state validation
Risk Level: Medium

Vulnerable Code

python
def login(self):
    """Login to RouterOS"""
    if not self.sock:
        return False
        
    try:
        # Send login command
        self._send_word('/login')
        self._send_word(f'=name={self.username}')
        self._send_word(f'=password={self.password}')
        self._send_word('')  # End marker
        
        # Receive response
        res = self._recv_response()
        
        # Check for !done response (login success marker)
        # res might look like [{'ret': '...'}, {}] or just []
        if res is not None:
            return True
            
        # If no !done but has data, also consider success (compatible with old versions)
        if res:
            return True
            
        # No response at all, might be a connection issue
        return False

The response parser also terminates on either success or error without preserving the status for the caller:

python
if word_type in ['!done', '!trap', '!halt']:
    # Completion/error message, return results
    break

Technical Analysis

_recv_response() returns a list in normal operation, including an empty list when the response times out, when the connection ends, or when RouterOS returns an error without parsed attributes. It does not return None for these ordinary failure paths.

Therefore, the condition if res is not None evaluates to true for both successful and unsuccessful authentication responses. In addition, _recv_response() discards whether the terminal response type was !done, !trap, or !halt. The caller cannot distinguish successful authentication from rejected credentials or an interrupted exchange.

This is a fail-open authentication-state error in the client. It d ...[truncated 1396 chars]

Remediation
View remediation

Remediation Suggestions

  • Return a structured response containing the terminal RouterOS response type.
  • Treat authentication as successful only after receiving an explicit, well-formed !done.
  • Treat !trap, !halt, timeouts, malformed responses, and connection closure as failures.
  • Preserve and safely surface RouterOS error messages without exposing credentials.
  • Remove the redundant if res branch after the broader if res is not None check.
  • Close the socket immediately when authentication fails.
  • Add unit and integration tests covering successful login, invalid credentials, !trap, !halt, timeout, partial response, malformed response, and connection closure.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
Findings (18)

Missing User Warnings

High
Category
Not specified by scanner
Confidence
95% confidence
Finding

The README advertises support for cleartext RouterOS API connections without warning that authentication material and management traffic may traverse the network unencrypted. For a router administration client, this creates substantial risk of credential theft and session monitoring on untrusted or shared networks, especially because the examples normalize insecure connectivity.

Content

No source excerpt is available for this finding.

Missing User Warnings

High
Category
Not specified by scanner
Confidence
95% confidence
Finding

The shutdown method executes /system/shutdown, an irreversible availability-impacting action, without any confirmation, print/log statement, or explicit warning describing the consequence. A bare docstring of 'Shutdown device' does not adequately disclose the operational impact to users invoking the skill.

Content

No source excerpt is available for this finding.

Tool Parameter Abuse

High
Category
Tool Misuse
Confidence
90% confidence
Finding

Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Content

Scanner excerpt · scanner.py (reported line 28)May include surrounding context.

python
subnets = []
        try:
            # For Windows
            output = subprocess.check_output("ipconfig", shell=True).decode('gbk', errors='ignore')
            ip_matches = re.findall(r'IPv4 Address.*?: (\d+\.\d+\.\d+\.\d+)', output)
            mask_matches = re.findall(r'Subnet Mask.*?: (\d+\.\d+\.\d+\.\d+)', output)

Tool Parameter Abuse

High
Category
Tool Misuse
Confidence
90% confidence
Finding

Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Content

Scanner excerpt · scanner.py (reported line 40)May include surrounding context.

python
except:
            # Fallback for Linux
            try:
                output = subprocess.check_output("ip addr", shell=True).decode()
                matches = re.findall(r'inet (\d+\.\d+\.\d+\.\d+/\d+)', output)
                for m in matches:
                    if not m.startswith('127.'):

Tool Parameter Abuse

High
Category
Tool Misuse
Confidence
99% confidence
Finding

Interpolating ip into arp -a {ip} while using shell=True is a classic command injection pattern. In an agent skill that scans networks, any path allowing external control of subnet/ip values can turn a reconnaissance utility into arbitrary command execution on the host running the skill.

Content

Scanner excerpt · scanner.py (reported line 79)May include surrounding context.

python
def _get_mac_from_arp(self, ip):
        """Try to get MAC address from system ARP table"""
        try:
            output = subprocess.check_output(f"arp -a {ip}", shell=True).decode('gbk', errors='ignore')
            match = re.search(r'([0-9a-fA-F]{2}[:-]){5}([0-9a-fA-F]{2})', output)
            if match:
                return match.group(0).replace('-', ':').lower()

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The README explicitly shows passing the router password via a command-line flag, which can expose secrets through shell history, terminal logging, and process listings visible to other local users. Because this is documentation for an administrative network tool, users are likely to copy-paste the example directly, making credential disclosure a realistic risk.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

The skill explicitly advertises execution of arbitrary RouterOS commands but does not warn that many RouterOS operations are state-changing and can alter firewall rules, routing, users, or device availability. In a network-device management context, this omission is dangerous because users may invoke destructive or risky commands through the agent without understanding the potential operational impact.

Content

No source excerpt is available for this finding.

Ssd 3

Medium
Category
Not specified by scanner
Confidence
97% confidence
Finding

The documentation instructs users to place device hostnames, usernames, and even blank or plaintext passwords in TOOLS.md, which normalizes insecure credential handling. This creates a significant risk of credential disclosure through local files, backups, version control, shared workspaces, or downstream agent access, and the example of an admin account with an empty password is especially unsafe.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
88% confidence
Finding

The skill promotes backup and storage-cleanup capabilities without clearly warning that these features perform filesystem writes and may delete files or affect limited router storage. On infrastructure devices, cleanup and backup actions can consume space, remove needed artifacts, or change operational state, especially if invoked broadly or on the wrong device.

Content

No source excerpt is available for this finding.

Unbounded Resource Access

Medium
Category
Excessive Agency
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.

Content

Scanner excerpt · client.py (reported line 186)May include surrounding context.

python
entry[parts[1]] = parts[2]
                
                # Reduce subsequent timeout
                current_timeout = 0.5
                
            return results
        except Exception as e:

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
97% confidence
Finding

The login method effectively treats any non-None response as authentication success, without verifying that RouterOS actually returned a success marker such as !done and no !trap error. This can cause callers to believe they are authenticated when they are not, potentially bypassing application-level security checks or causing unsafe logic to proceed under false assumptions.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The client sends the username and password over the RouterOS API connection on port 8728 by default, which is the non-SSL service, and there is no enforcement or warning to use TLS. In real deployments this can expose credentials to network eavesdropping or interception, especially on untrusted or shared networks.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

The reboot method invokes /system/reboot, which is a disruptive and potentially safety-critical operation, but the code provides only a minimal docstring and no confirmation prompt, logging, or explicit warning to the user about device interruption. In this file, comparable user-facing disclosure appears only in print_status, not around the destructive action itself.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scanner.py (reported line 28)May include surrounding context.

python
subnets = []
        try:
            # For Windows
            output = subprocess.check_output("ipconfig", shell=True).decode('gbk', errors='ignore')
            ip_matches = re.findall(r'IPv4 Address.*?: (\d+\.\d+\.\d+\.\d+)', output)
            mask_matches = re.findall(r'Subnet Mask.*?: (\d+\.\d+\.\d+\.\d+)', output)

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scanner.py (reported line 40)May include surrounding context.

python
except:
            # Fallback for Linux
            try:
                output = subprocess.check_output("ip addr", shell=True).decode()
                matches = re.findall(r'inet (\d+\.\d+\.\d+\.\d+/\d+)', output)
                for m in matches:
                    if not m.startswith('127.'):

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
99% confidence
Finding

This command interpolates the ip value into a shell command with shell=True, creating a command injection sink if ip can be influenced by untrusted input. In this file ip is usually derived from local subnet enumeration, but the method is a reusable class method and scan_subnet accepts arbitrary subnet strings, so crafted values could reach this path and execute arbitrary OS commands.

Content

Scanner excerpt · scanner.py (reported line 79)May include surrounding context.

python
def _get_mac_from_arp(self, ip):
        """Try to get MAC address from system ARP table"""
        try:
            output = subprocess.check_output(f"arp -a {ip}", shell=True).decode('gbk', errors='ignore')
            match = re.search(r'([0-9a-fA-F]{2}[:-]){5}([0-9a-fA-F]{2})', output)
            if match:
                return match.group(0).replace('-', ':').lower()

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
88% confidence
Finding

This code actively scans all detected local subnets and probes hosts on MikroTik API ports, which is a network operation that can affect privacy or trigger monitoring, yet the scan is launched immediately when executed. While there is a progress print, it does not meaningfully warn the user in advance about enumerating the local network or performing ARP lookups on discovered hosts.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Low
Category
Not specified by scanner
Confidence
98% confidence
Finding

The docstring for scan_subnet says it scans ports 8728 and 8729, and nearby comments repeat that claim. In the actual code, only one thread is created per IP and it calls _check_port with port 8728; there is no corresponding probe for 8729.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.