Back to skill

Security audit

ratsinfo-de

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly matches its stated civic-records purpose, but its promised same-host network boundary can be bypassed through redirects, so it should be reviewed before installation.

Install only if you are comfortable with a CLI that makes public web requests to OParl endpoints and dev.oparl.org, writes a local cache, and can create user-named PDF or calendar files. Avoid untrusted endpoints and untrusted logo PNG files until redirect handling is fixed to validate before following redirects and the logo parser has size limits.

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/ratsinfo.py:205
Finding
Cross-Host Restriction Bypass Through Automatic HTTP Redirects<![CDATA[ ## Vulnerability Details **File Location**: `scripts/ratsinfo.py:69-75, 149-155, 205-216` **Vulnerability Type**: Server-Side Request Forgery through redirects **Risk Level**: High ### Vulnerable Code The shared opener enables automatic redirects: ```python _OPENER = urllib.request.build_opener( urllib.request.HTTPHandler, urllib.request.HTTPSHandler, urllib.request.HTTPRedirectHandler, urllib.request.HTTPErrorProcessor, ) ``` The main JSON-fetching path validates the final host only after the redirect-capable opener has completed the request: ```python req = urllib.request.Request( url, headers={ "User-Agent": USER_AGENT, "Accept": "application/json, */*;q=0.5", }, method="GET", ) with _OPENER.open(req, timeout=TIMEOUT) as resp: final = resp.geturl() if _host(final) != _host(url): _check_url(final) # a redirect off host needs consent too raw = resp.read(MAX_BYTES + 1) ``` The `robots.txt` path uses the same opener and does not validate its final URL: ```python req = urllib.request.Request( robots_url, headers={"User-Agent": USER_AGENT} ) with _OPENER.open(req, timeout=TIMEOUT) as resp: body = resp.read(1024 * 1024).decode("utf-8", "replace") parser.parse(body.splitlines()) ``` ### Technical Analysis The Skill attempts to restrict requests to the host explicitly selected by the user. However, `urllib.request.HTTPRedirectHandler` follows HTTP redirects inside `_OPENER.open()` before control returns to the caller. Consequently, when this code evaluates `resp.geturl()` and invokes `_check_url(final)`, the redirected request has already been transmitted. The check can prevent processing of the response, but it cannot prevent the cross-host network interaction. The `robots.txt` request is more exposed because it uses the same redirect-capable opener without performing any final-host validation. Only URL schemes are restricted. The implementation does not reject loopbac ...[truncated 1852 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Replace the permissive redirect handler with a custom handler that validates every redirect target before following it. 2. Call `_check_url()` from `redirect_request()` or an equivalent pre-request interception point, rather than checking only after `_OPENER.open()` returns. 3. Use the same validated redirect policy for both OParl requests and `robots.txt`. 4. Reject loopback, private, link-local, multicast, unspecified, and reserved IP destinations by default. 5. Resolve hostnames and validate all returned addresses before connecting. Revalidate the connected destination where practical to reduce DNS rebinding exposure. 6. Consider disabling redirects by default and handling each redirect explicitly with a small maximum redirect count. 7. Permit cross-host redirects only when `--allow-cross-host` is explicitly provided. 8. Add regression tests proving that an off-host redirect does not cause any connection to the target server. 9. Add separate tests for endpoint, pagination, object-dereference, and `robots.txt` redirects. 10. Update the documentation only after the restriction is enforced before network transmission. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/pdfreport.py:45
Finding
Unbounded PNG Decompression and Memory Allocation<![CDATA[ ## Vulnerability Details **File Location**: `scripts/pdfreport.py:45-92` **Vulnerability Type**: Resource-exhaustion denial of service **Risk Level**: Medium ### Vulnerable Code The optional logo parser reads the complete input and accumulates all compressed image data without a size limit: ```python with open(path, "rb") as handle: data = handle.read() if data[:8] != b"\x89PNG\r\n\x1a\n": raise ValueError(f"{path} is not a PNG") pos = 8 width = height = depth = colour = 0 idat = bytearray() while pos < len(data): (length,) = struct.unpack(">I", data[pos:pos + 4]) kind = data[pos + 4:pos + 8] body = data[pos + 8:pos + 8 + length] pos += 12 + length if kind == b"IHDR": width, height, depth, colour = struct.unpack(">IIBB", body[:10]) elif kind == b"IDAT": idat += body elif kind == b"IEND": break ``` It then performs unrestricted decompression and allocates buffers using dimensions controlled by the file: ```python raw = zlib.decompress(bytes(idat)) stride = width * channels out = bytearray() previous = bytearray(stride) pos = 0 for _ in range(height): filt = raw[pos] pos += 1 line = bytearray(raw[pos:pos + stride]) pos += stride for i in range(stride): left = line[i - channels] if i >= channels else 0 up = previous[i] upleft = previous[i - channels] if i >= channels else 0 if filt == 1: line[i] = (line[i] + left) & 0xFF elif filt == 2: line[i] = (line[i] + up) & 0xFF elif filt == 3: line[i] = (line[i] + ((left + up) >> 1)) & 0xFF elif filt == 4: line[i] = (line[i] + _paeth(left, up, upleft)) & 0xFF out += line previous = line rgb = bytearray(width * height * 3) ``` ### Technical Analysis The PNG parser imposes no limits on: - Input file size. - Accumulated `IDAT` size. - Decompressed byte count. - Image width or height. - Total pixel count. - Compression ...[truncated 2147 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Reject logo files larger than a conservative maximum before reading them, such as 5 MiB. 2. Validate PNG chunk boundaries and reject truncated, malformed, or excessively large chunks. 3. Require exactly one valid `IHDR` chunk before processing image data. 4. Set maximum width, height, and total pixel count, for example: - Width and height no greater than 4096. - Total pixels no greater than 16 million. 5. Calculate the expected decompressed scanline size safely before decompression: `height * (1 + width * channels)`. 6. Reject images whose expected size exceeds a configured memory budget. 7. Replace unrestricted `zlib.decompress()` with incremental decompression using `zlib.decompressobj()` and enforce a strict output limit. 8. Verify that decompressed output length exactly equals the expected PNG scanline length. 9. Avoid retaining duplicate full-size buffers where possible; process scanlines incrementally into the final RGB representation. 10. Catch `MemoryError` at the report boundary and return a controlled error, while recognizing that preventive limits are the primary defense. 11. Add tests using oversized dimensions, truncated chunks, excessive `IDAT` data, and high-expansion compressed streams. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • System Prompt LeakageDirect Leakage, Indirect Extraction, Tool-Based Exfiltration
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
Findings (38)

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding
The declared purpose is a data-access/retrieval skill focused on reading German municipal council records through the OParl API. The actual code chunk is unrelated infrastructure for producing PDF reports. It includes PNG parsing, text wrapping, page layout, footer/header rendering, and PDF serialization to a file path. These are materially different capabilities from the declared purpose, and key declared behaviors—API access, municipal record lookup, topic filtering, committee/member lookup, or monitoring new papers—are absent. This is therefore a clear description-behavior mismatch.

Tp4

High
Category
MCP Tool Poisoning
Confidence
94% confidence
Finding
The declared purpose is a user-facing skill for reading and tracking German municipal council records through OParl. This code chunk does not primarily implement that behavior; instead, it records a demo session and orchestrates integration tests. It launches mock servers, invokes a CLI repeatedly, checks healthy and broken endpoints, writes temp files, runs a test script, and captures outputs. Most notably, it includes a 'get file:///etc/passwd' command, which indicates local file retrieval behavior not reflected in the description. While some invoked commands (papers, track, watch, scan, report) are related to council-record functionality, the chunk’s actual purpose and capabilities are test/demo automation with additional filesystem and diagnostic actions, so the description does not accurately represent this code.

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding
This code does not implement or support the declared behavior of querying or reading German municipal council records through the OParl API. Its primary purpose is quality assurance for generated PDF dossiers: validating that a PDF contains expected headings and terms. It accesses a local file path from argv, performs PDF content extraction, and exits with validation errors if expected phrases are missing. That is a materially different purpose and resource usage from the declared skill description.

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding
The declared purpose is to access and interpret German municipal council information via the OParl API. This code does not query OParl, municipalities, committees, papers, or any civic records at all. Its primary function is unrelated: generating a demo video from a prerecorded local session transcript. It accesses local files, image rendering libraries, temporary directories, and ffmpeg for video creation, which are materially different resources and capabilities from the declared council-record lookup behavior. This is a clear description-behavior mismatch.

Tp4

High
Category
MCP Tool Poisoning
Confidence
91% confidence
Finding
The code broadly aligns with the declared domain: it accesses municipal council information from OParl/RIS-style endpoints and supports papers, meetings, bodies, people, and tracking decisions. However, the code chunk shows materially broader capabilities than the description states. In particular, it can generate PDF dossier/report outputs, create ICS calendar files, and scan multiple municipality endpoints to produce comparative/market reports. Those are substantive user-facing capabilities, not merely internal implementation details. The description mentions being told about new council papers on a subject, but not calendar export, PDF generation, or bulk cross-municipality reporting. Therefore the declared description understates important actual behaviors, so this should be flagged as a mismatch.

Credential Access

High
Category
Privilege Escalation
Content
# Only http and https. Building our own opener removes urllib's file and ftp
# handlers entirely, so a hostile endpoint cannot point links.next at
# file:///home/you/.ssh/id_rsa and have us read it back to the agent.
_OPENER = urllib.request.build_opener(
    urllib.request.HTTPHandler,
    urllib.request.HTTPSHandler,
Confidence
90% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
# Only http and https. Building our own opener removes urllib's file and ftp
# handlers entirely, so a hostile endpoint cannot point links.next at
# file:///home/you/.ssh/id_rsa and have us read it back to the agent.
_OPENER = urllib.request.build_opener(
    urllib.request.HTTPHandler,
    urllib.request.HTTPSHandler,
Confidence
90% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Env Variable Harvesting

High
Category
Data Exfiltration
Content
R = ["python3", "scripts/ratsinfo.py"]

def town(port, name, ags):
    env = dict(os.environ, MOCK_PORT=str(port), MOCK_TOWN=name, MOCK_AGS=ags)
    return subprocess.Popen(["python3", "tests/mock_ris.py"], env=env)
Confidence
60% confidence
Finding
Code enumerates, copies, or searches environment variables for secrets. Bulk environment access can collect credentials unrelated to the skill's stated purpose.

Credential Access

High
Category
Privilege Escalation
Content
R + ["watch", OK, "--term", "Sanierung"],
    R + ["check", BAD + "/dead"],
    R + ["check", BAD + "/blocked"],
    R + ["get", "file:///etc/passwd"],
    R + ["scan", "--endpoints", "/tmp/eps.txt", "--term", "Sanierung",
         "--pdf", "/tmp/markt.pdf", "--logo", "/tmp/aq.png", "--org",
         "ALPHA QUADRAT  Bauplanung und Projektmanagement"],
Confidence
97% confidence
Finding
The demo explicitly invokes the skill with file:///etc/passwd, demonstrating and validating local file read capability unrelated to the stated municipal-records purpose. In an agent environment, accepting file:// URLs can enable arbitrary local file disclosure, and /etc/passwd is a classic probe indicating filesystem access testing rather than legitimate business logic.

Direct Prompt Extraction

High
Category
System Prompt Leakage
Content
if stripped.startswith("error:") or "passed," in line and "0 failed" not in line:
        return ERR
    if stripped.startswith("$"):
        return PROMPT
    return OUT
Confidence
85% confidence
Finding
Skill contains instructions that could directly expose system prompts, internal rules, or hidden instructions to users or external parties.

Chaining Abuse

High
Category
Tool Misuse
Content
MOCK_T2=$!
python3 tests/mock_broken.py &
MOCK_BAD=$!
trap 'kill $MOCK_OK $MOCK_T2 $MOCK_BAD 2>/dev/null; rm -rf "$RATSINFO_CACHE"' EXIT
sleep 1

PASS=0
Confidence
75% confidence
Finding
Tool calls are chained to bypass individual safety checks or escalate capabilities beyond what any single tool call would allow.

Credential Access

High
Category
Privilege Escalation
Content
# --- the guards. each of these caught a real defect during review. ---
expect "a file:// url is refused" 2 "Only http and https" \
    python3 scripts/ratsinfo.py get "file:///etc/passwd"
expect "a cross host link is not followed" 2 "not the endpoint you asked for" \
    python3 scripts/ratsinfo.py check "http://127.0.0.1:8799/evil-system"
expect "--allow-cross-host lets it through" 2 "body list is empty" \
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Lp3

Medium
Category
MCP Least Privilege
Confidence
87% confidence
Finding
The skill advertises network access, cache/file writes, environment-variable control, and shell execution via Python, but it does not declare a constrained tool scope such as allowed tools or permissions. That increases the blast radius if the skill is invoked in a broader agent runtime, because the runtime may permit more capabilities than are necessary for the stated use case.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
The documentation states that `report` writes a one-page PDF dossier 'in German', which imposes a specific language on generated output. The file does not indicate any user opt-in, alternative locale support, or justification as a strict region-specific compliance requirement for the output language itself.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
such as expert reports, photographs and plans often carry third party
copyright, and paper texts routinely name private individuals who were
never public figures. A full text index of those names changes their
situation without asking them.

Read the `license` field, keep the source with every document you store,
and think before you publish. `references/recht-und-etikette.md` has the
Confidence
75% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Tainted flow: 'tmp' from os.environ.get (line 744, credential/environment) → open (file write)

Medium
Category
Data Flow
Content
def save_state(endpoint: str, kind: str, state: dict) -> None:
    path = _state_path(endpoint, kind)
    tmp = path + ".tmp"
    with open(tmp, "w", encoding="utf-8") as handle:
        json.dump(state, handle, ensure_ascii=False, indent=1)
    os.replace(tmp, path)  # never leave a half written state behind
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Tainted flow: 'tmp' from os.environ.get (line 744, credential/environment) → open (file write)

Medium
Category
Data Flow
Content
def save_state(endpoint: str, kind: str, state: dict) -> None:
    path = _state_path(endpoint, kind)
    tmp = path + ".tmp"
    with open(tmp, "w", encoding="utf-8") as handle:
        json.dump(state, handle, ensure_ascii=False, indent=1)
    os.replace(tmp, path)  # never leave a half written state behind
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Description-Behavior Mismatch

Medium
Confidence
92% confidence
Finding
The skill goes beyond read/query behavior by writing PDF dossiers and calendar files to arbitrary user-supplied local paths. In an agent setting, filesystem write capabilities expand the blast radius from passive data retrieval to persistent local side effects, which can overwrite files, leak queried data into artifacts, or be abused by downstream prompts to place files in sensitive locations.

Description-Behavior Mismatch

Medium
Confidence
89% confidence
Finding
The scan command performs broad multi-endpoint reconnaissance across many municipalities and can generate market-intelligence style reports, which materially exceeds the manifest’s municipality-specific lookup framing. In an agent context, this increases privacy/compliance and misuse risk because a seemingly narrow civic-records skill can be repurposed for bulk monitoring and profiling at scale.

Natural-Language Policy Violations

Medium
Confidence
86% confidence
Finding
This code declares the tool as a client for German council information systems, and several user-facing outputs and report text are hard-coded in German. The skill does not offer the user a language choice or opt-in for this locale, which matches the policy concern about forcing a specific language without user choice.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
os.environ["RATSINFO_MIN_INTERVAL"] = "0"
os.environ["RATSINFO_CACHE"] = "/tmp/ratsinfo-demo-cache"
subprocess.run(["rm", "-rf", "/tmp/ratsinfo-demo-cache"])

OK = "http://127.0.0.1:8799/system"
BAD = "http://127.0.0.1:8798"
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The script deletes /tmp/ratsinfo-demo-cache with rm -rf, which is a destructive file operation. Although the module docstring describes recording a demo session, there is no specific warning, confirmation, or inline comment disclosing that the script will remove filesystem data at this path.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def town(port, name, ags):
    env = dict(os.environ, MOCK_PORT=str(port), MOCK_TOWN=name, MOCK_AGS=ags)
    return subprocess.Popen(["python3", "tests/mock_ris.py"], env=env)


servers = [
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
town(8799, "Stadt Musterhausen", "05124000"),
    town(8801, "Stadt Beispielstadt", "05158000"),
    town(8802, "Gemeinde Musterdorf", "05170032"),
    subprocess.Popen(["python3", "tests/mock_broken.py"]),
]
open("/tmp/eps.txt", "w").write(
    "http://127.0.0.1:8799/system\n"
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Context-Inappropriate Capability

Medium
Confidence
89% confidence
Finding
The demo script intentionally exercises capabilities outside the municipal-records use case, including local file access and execution of additional scripts. In an agent-skill context, showcasing and normalizing such behavior can mask dangerous capability creep and encourages paths that could expose host data if similar functionality is reachable in production.

Static analysis

Detected: suspicious.dynamic_code_execution

Dynamic code execution detected.

Critical
Code
suspicious.dynamic_code_execution
Location
tests/run_tests.sh:155