Back to skill

Security audit

RTL-SDR WFM RDS Decoder

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly matches its FM recording purpose, but it can run an unverified redsea binary from a predictable /tmp path, which creates a real local code-execution risk.

Install only if you trust the local machine and either have redsea installed from a trusted PATH location or remove/patch the /tmp fallback. Avoid running this skill as a privileged user, and expect it to create MP3, JSON cache, scan summary, and debug files in the selected output directory.

Vulnerability Patterns
  • Tool Hijacking and SpoofingModifies or replaces tools so legitimate-looking calls execute attacker logic
  • 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 (1)

T07 · Tool Hijacking and Spoofing

Error
Location
scripts/fm_config.py:31
Finding

Execution of an Unverified Binary from a Predictable Temporary Directory

Content
View full analysis

Vulnerability Details

File Location: scripts/fm_config.py:31; scripts/fm_iq_pipeline.py:87-94, 151, 258-260
Vulnerability Type: Local tool hijacking through an unsafe temporary-directory fallback
Risk Level: High

Vulnerable Code

scripts/fm_config.py:31:

python
DEFAULT_REDSEA_PATH = Path('/tmp/redsea/build/redsea')

scripts/fm_iq_pipeline.py:87-94:

python
def resolve_redsea_bin() -> Path | None:
    redsea_in_path = shutil.which('redsea')
    if redsea_in_path:
        return Path(redsea_in_path)
    if DEFAULT_REDSEA_PATH.exists():
        return DEFAULT_REDSEA_PATH
    return None

scripts/fm_iq_pipeline.py:151:

python
redsea_probe = probe_tool([str(redsea_bin), '--help']) if redsea_bin is not None else {'available': False, 'probeMessage': 'Not found in PATH or fallback location'}

scripts/fm_iq_pipeline.py:258-260:

python
proc = subprocess.Popen(
    [str(redsea_bin), '-r', '171000', '-p'],

Technical Analysis

When redsea is unavailable through PATH, the application falls back to the fixed path /tmp/redsea/build/redsea. The resolver verifies only that this path exists. It does not verify that the file:

  • Is a regular file rather than a symbolic link or another special file.
  • Is owned by a trusted user.
  • Has safe ownership and permission settings.
  • Resides beneath directories that are not writable by untrusted users.
  • Matches an expected cryptographic hash or trusted package.
  • Remains unchanged between validation and execution.

/tmp is conventionally writable by local users. Although its sticky bit restricts deletion of entries belonging to other users, it does not prevent an attacker from creating a previously absent predictable directory hierarchy. If /tmp/redsea does not already exist with secure ownership and permissions, an attacker can create the fallback path and place an executable there.

The u ...[truncated 2268 chars]

Remediation
View remediation

Remediation Suggestions

  1. Remove the /tmp fallback and require redsea to be installed through a trusted system package or in an explicitly configured, administrator-controlled location.

    python
    def resolve_redsea_bin() -> Path | None:
        candidate = shutil.which("redsea")
        return Path(candidate).resolve() if candidate else None
    
  2. If a fallback is operationally necessary, move it outside shared temporary storage, such as /usr/local/bin/redsea or an application-owned directory with restrictive permissions.

  3. Before execution, validate the candidate and every parent directory:

    • Resolve symbolic links with Path.resolve(strict=True).
    • Require a regular executable file.
    • Reject symbolic links.
    • Require ownership by an expected trusted UID.
    • Reject group-writable or world-writable files.
    • Reject paths beneath group-writable or world-writable directories.
    • Optionally verify a pinned SHA-256 digest or package signature.
  4. Avoid time-of-check/time-of-use replacement between validation and execution. Where practical, open the verified executable with safe flags such as O_NOFOLLOW, validate it with fstat(), and execute the same opened object through an appropriate platform mechanism. Otherwise, ensure the entire installation directory is protected against modification by untrusted users.

  5. Apply the same verification before both probe execution and normal RDS decoding. A --help probe still executes the selected binary and must not be treated as harmless.

  6. Update the documentation to require a trusted, package-managed installation rather than recommending a binary under /tmp.

Vulnerability Patterns
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (16)

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding

The declared description is largely accurate about the primary purpose: the script does record broadcast FM from RTL-SDR, uses IQ capture, performs offline WBFM demodulation, extracts RDS station names, supports scanning, and names recordings with station/frequency/time information. However, the implementation also includes notable undeclared capabilities: a dedicated environment check that probes installed tools and versions, plus persistent creation of auxiliary JSON cache/summary/debug metadata files. These are not merely tiny internal details because they are exposed as user-invokable actions (--check) and produce additional outputs. The primary purpose still aligns well, so this is a mild but real description-behavior mismatch.

Content

No source excerpt is available for this finding.

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 18)May include surrounding context.

md
5. Use `scripts/fm_iq_pipeline.py` for recording.

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 43)May include surrounding context.

md
5. Use `scripts/fm_iq_pipeline.py` for recording.

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 51)May include surrounding context.

md
5. Use `scripts/fm_iq_pipeline.py` for recording.

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 57)May include surrounding context.

md
5. Use `scripts/fm_iq_pipeline.py` for recording.

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 65)May include surrounding context.

md
5. Use `scripts/fm_iq_pipeline.py` for recording.

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 71)May include surrounding context.

md
5. Use `scripts/fm_iq_pipeline.py` for recording.

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 185)May include surrounding context.

md
5. Use `scripts/fm_iq_pipeline.py` for recording.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
70% confidence
Finding

Without declared permissions the skill's intent is opaque and cannot be validated.

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 · scripts/fm_iq_pipeline.py (reported line 101)May include surrounding context.

python
def detect_tool_version(command: list[str], banner_prefixes: tuple[str, ...] = ()) -> str | None:
    try:
        result = subprocess.run(command, capture_output=True, text=True, timeout=VERSION_PROBE_TIMEOUT_SEC)
    except (OSError, subprocess.SubprocessError):
        return None

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 · scripts/fm_iq_pipeline.py (reported line 127)May include surrounding context.

python
def probe_tool(command: list[str]) -> dict:
    try:
        result = subprocess.run(command, capture_output=True, text=True, timeout=RTL_HELP_TIMEOUT_SEC)
    except subprocess.TimeoutExpired:
        return {'available': False, 'probeMessage': 'Timed out during probe'}
    except OSError as exc:

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 · scripts/fm_iq_pipeline.py (reported line 188)May include surrounding context.

python
def run(cmd: list[str]) -> None:
    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0:
        details = [f"Command failed ({result.returncode}): {' '.join(cmd)}"]
        if result.stdout:

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 · scripts/fm_iq_pipeline.py (reported line 245)May include surrounding context.

python
result.rejection_reason = 'redsea-not-found'
        return "UnknownStation", build_rds_debug_info(freq_mhz, timeout_sec, evidence, result)

    rtl_fm_proc = subprocess.Popen(
        [
            'rtl_fm', '-M', 'fm', '-l', '0', '-A', 'std', '-p', '0', '-s', '171k',
            '-g', str(gain_db), '-F', '9', '-f', f'{freq_mhz}M'

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 · scripts/fm_iq_pipeline.py (reported line 258)May include surrounding context.

python
)
    if rtl_fm_proc.stdout is None:
        raise RuntimeError("rtl_fm stdout pipe was not created")
    proc = subprocess.Popen(
        [str(redsea_bin), '-r', '171000', '-p'],
        stdin=rtl_fm_proc.stdout,
        stdout=subprocess.PIPE,

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 · scripts/fm_iq_pipeline.py (reported line 292)May include surrounding context.

python
if not redsea_stdout_open and not rtl_fm_stderr_open:
                break

            events = selector.select(timeout=0.5)
            for key, _ in events:
                chunk = os.read(key.fd, 4096)
                if not chunk:

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
94% confidence
Finding

The comment and constant set a fixed FM deemphasis time constant specifically for Europe/Poland. This is a natural-language locale constraint embedded in the file, and there is no indication here that users can select a different regional standard or that the limitation is surfaced as an explicit opt-in.

Content

No source excerpt is available for this finding.