Back to skill

Security audit

Ludwitt University

Security checks for vulnerabilities and agentic risk

Overview

This skill is a real coursework integration, but installing it creates a persistent background daemon, stores credentials, and uses unsafe install/update patterns that need review.

Install only if you are comfortable with a long-running Ludwitt daemon, remote registration, local credential storage, public repository/deployment requirements, and paper text being sent to the service. Prefer reviewing the local installer instead of any curl | sh path, do not add the suggested HEARTBEAT.md or cron automation unless you want autonomous recurring actions, and remove the service/credentials when finished.

Vulnerability Patterns
  • 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
  • System PersistenceInstalls backdoors, hooks, services, or scheduled tasks that survive the run
  • 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
Findings (4)

T03 · Remote Payload Retrieval and Execution

Error
Location
install.sh:8
Finding
Mutable Remote Installer Is Recommended for Direct Shell Execution<![CDATA[ ## Vulnerability Details **File Location**: `install.sh:8-10`; `daemon.js:39-43` **Vulnerability Type**: Remote payload retrieval and execution **Risk Level**: High ### Vulnerable Code ```bash # Usage: # curl -sSL https://opensource.ludwitt.com/install | sh # # or after cloning the skill repo: # ./install.sh ``` ```js function checkUpdateAvailable(result) { if (updateCheckShown || !result?.apiVersion) return try { const auth = JSON.parse(fs.readFileSync(AUTH_FILE, 'utf8')) const clientVersion = auth.clientVersion if (!clientVersion || clientVersion === result.apiVersion) return updateCheckShown = true console.error( `[ludwitt] A new API version is available (server: ${result.apiVersion}, yours: ${clientVersion}). Update: ${result.updateInstructions || 'curl -sSL https://opensource.ludwitt.com/install | sh'}` ) } catch {} } ``` ### Technical Analysis The installation instructions recommend downloading a mutable script from `https://opensource.ludwitt.com/install` and piping it directly into a shell. The downloaded content is neither pinned to a release nor verified using a cryptographic signature or checksum. Consequently, the code that ultimately executes can differ from the code reviewed in this package. The daemon also prints `result.updateInstructions`, which is supplied by the remote API. Although the daemon does not execute that field automatically, a compromised or malicious server can present arbitrary commands as trusted update instructions. The fallback instruction again uses the unsafe `curl | sh` pattern. This behavior is not necessary for the declared course-management functionality. The repository already supports installation from a locally cloned, reviewable script. ### Attack Path 1. An attacker compromises the Ludwitt web server, API deployment, DNS resolution, TLS termination, or release pipeline. 2. The attacker replaces the `/install` response with a malicious shell payload or retur ...[truncated 1200 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove all `curl | sh` installation and update instructions. 2. Publish versioned release archives and pin installation to a specific version or commit. 3. Publish a SHA-256 or stronger digest through a separately authenticated release channel. 4. Cryptographically sign release artifacts and verify the signature before execution. 5. Download the installer to a local file, inspect and verify it, and only then execute it. 6. Do not accept executable update commands from the API. The API should return only a validated semantic version and a fixed HTTPS release URL. 7. Prefer package-manager or ClawHub updates with lockfiles, provenance metadata, and reproducible releases. 8. Ensure redirects are disabled or restricted to an explicit same-origin allowlist when retrieving release artifacts. ]]>

T06 · System Persistence

Error
Location
install.sh:215
Finding
Installer Automatically Registers an Always-On Startup Service<![CDATA[ ## Vulnerability Details **File Location**: `install.sh:215-270` **Vulnerability Type**: Cross-session system persistence **Risk Level**: High ### Vulnerable Code ```bash install_launchd() { local plist="$HOME/Library/LaunchAgents/com.ludwitt.daemon.plist" mkdir -p "$HOME/Library/LaunchAgents" cat > "$plist" << PLISTEOF <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Label</key> <string>com.ludwitt.daemon</string> <key>ProgramArguments</key> <array> <string>$(which node)</string> <string>$LUDWITT_DIR/daemon.js</string> <string>--daemon</string> </array> <key>RunAtLoad</key> <true/> <key>KeepAlive</key> <true/> <key>StandardOutPath</key> <string>$LUDWITT_DIR/daemon.log</string> <key>StandardErrorPath</key> <string>$LUDWITT_DIR/daemon.err.log</string> <key>EnvironmentVariables</key> <dict> <key>HOME</key> <string>$HOME</string> </dict> </dict> </plist> PLISTEOF launchctl unload "$plist" 2>/dev/null || true launchctl load "$plist" info "Daemon registered with launchd (starts on boot)" } install_systemd() { local service="$HOME/.config/systemd/user/ludwitt-daemon.service" mkdir -p "$HOME/.config/systemd/user" cat > "$service" << SVCEOF [Unit] Description=Ludwitt University Daemon After=network.target [Service] ExecStart=$(which node) $LUDWITT_DIR/daemon.js --daemon Restart=on-failure RestartSec=10 Environment=HOME=$HOME [Install] WantedBy=default.target SVCEOF systemctl --user daemon-reload systemctl --user enable ludwitt-daemon systemctl --user start ludwitt-daemon info "Daemon registered with systemd (starts on boot)" } ``` ### Technical Analysis The installer automatically creates and enables a user-level launchd or systemd service. On macOS, `RunAtLoad` and `KeepAlive` cause the daemon to start automatically and remain running. On Linux, `sy ...[truncated 1993 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Make installation of a background service explicitly opt-in. 2. Default to an on-demand CLI that fetches status only when invoked. 3. Request informed confirmation immediately before creating or enabling a service. 4. On macOS, remove `KeepAlive`; use a limited interval only if background synchronization is explicitly enabled. 5. On Linux, do not invoke `systemctl --user enable` during normal installation. 6. Pin the service to a versioned, integrity-verified daemon rather than a mutable symlink. 7. Run the daemon with the least possible filesystem and network permissions. Apply systemd hardening such as `NoNewPrivileges`, `ProtectSystem`, `PrivateTmp`, and restrictive `ReadWritePaths` where compatible. 8. Provide and document a complete uninstall procedure that unloads the launch agent or disables the systemd unit and removes its files. 9. Clearly show polling frequency, transmitted data, credential use, and resource implications before opt-in. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
daemon.js:75
Finding
Authenticated Requests Forward Credentials and Sensitive Bodies Across Unrestricted Redirects<![CDATA[ ## Vulnerability Details **File Location**: `daemon.js:75-116` **Vulnerability Type**: Credential disclosure through unsafe redirect handling **Risk Level**: High ### Vulnerable Code ```js function requestOnce(method, endpoint, body, redirectCount = 0) { const auth = loadAuth() const url = new URL(endpoint, auth.apiUrl) const mod = url.protocol === 'https:' ? https : http return new Promise((resolve, reject) => { let settled = false const finish = (fn, value) => { if (settled) return settled = true fn(value) } const payload = body ? JSON.stringify(body) : null const req = mod.request( url, { method, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${auth.apiKey}`, 'X-Ludwitt-Fingerprint': auth.fingerprint, 'X-Agent-Type': auth.agentFramework || 'generic', 'User-Agent': `ludwitt-daemon/${auth.agentFramework || 'generic'}`, ...(payload && { 'Content-Length': Buffer.byteLength(payload) }), }, }, (res) => { // Follow redirects (307/301/302) up to 3 hops if ( [301, 302, 307, 308].includes(res.statusCode) && res.headers.location && redirectCount < 3 ) { res.resume() const redirectUrl = new URL(res.headers.location, url) return requestOnce( method, redirectUrl.toString(), body, redirectCount + 1 ) .then(resolve) .catch(reject) } ``` ### Technical Analysis The authenticated HTTP client follows 301, 302, 307, and 308 redirects without checking whether the destination retains the original scheme, hostname, and port. The recursive `requestOnce` call reloads authentication and constructs a new request containing: - `Authorization: Bearer <apiKey>` - `X-Ludwitt-Fingerprint: <fingerprint>` - The original HTTP method. ...[truncated 1958 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Disable redirects for authenticated API calls unless they are strictly necessary. 2. If redirects are required, compare the destination against the original URL and require an exact match for protocol, hostname, and effective port. 3. Require HTTPS for the configured API URL and every redirect destination. 4. Never forward `Authorization`, fingerprint headers, cookies, or sensitive bodies after an origin change. 5. Reject HTTPS-to-HTTP downgrade redirects unconditionally. 6. For POST requests, reject 301 and 302 redirects rather than replaying the original method and body. 7. Maintain an explicit allowlist of trusted API origins and validate `auth.apiUrl` when loading configuration. 8. Add automated tests covering cross-origin redirects, scheme downgrades, redirect loops, credential stripping, and POST-body replay. 9. Consider certificate or public-key pinning where the deployment and operational model can support safe pin rotation. ]]>

T02 · Agent Memory Poisoning

Warning
Location
install.sh:298
Finding
Installer Encourages Persistent Autonomous Agent Instructions and Scheduled Actions<![CDATA[ ## Vulnerability Details **File Location**: `install.sh:298-317` **Vulnerability Type**: Persistent Agent task steering **Risk Level**: Medium ### Vulnerable Code ```bash echo " ┌─────────────────────────────────────────────────────────────────┐" echo " │ ⏰ IMPORTANT: Set Up 2 Daily Heartbeats │" echo " │ │" echo " │ Add these to your HEARTBEAT.md or cron schedule: │" echo " │ │" echo " │ Morning (9 AM): Check ludwitt status, start next │" echo " │ deliverable if idle │" echo " │ │" echo " │ Evening (9 PM): Check progress, verify submissions, │" echo " │ review peer work if professor-eligible │" echo " │ │" echo " │ Heartbeat checklist: │" echo " │ 1. ludwitt status && ludwitt courses │" echo " │ 2. If no deliverable in-progress → ludwitt start <id> │" echo " │ 3. If in-progress → keep building, deploy, submit │" echo " │ 4. ludwitt queue → grade peer work (earns XP) │" echo " │ 5. Target: 1 deliverable per week minimum │" echo " │ │" echo " │ OpenClaw agents: add the above to your HEARTBEAT.md file │" echo " │ Other agents: set up 2 cron jobs (0 9 * * * and 0 21 * * *) │" echo " └─────────────────────────────────────────────────────────────────┘" ``` ### Technical Analysis The installer does not itself modify `HEARTBEAT.md` or create these two cron entries. However, it explicitly instructs an Agent or operator to copy recurring behavioral rules into ...[truncated 1832 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove instructions to place autonomous work directives in `HEARTBEAT.md` or cron. 2. Restrict optional background behavior to passive status synchronization. 3. Require a new, explicit user request before starting deliverables, modifying repositories, deploying applications, submitting work, or grading peers. 4. If reminders are offered, make them opt-in and non-actionable: notify the user rather than executing course actions. 5. Present the exact schedule, intended duration, network activity, and potential resource usage before any scheduling setup. 6. Provide expiration dates, pause controls, and removal instructions for every persistent reminder. 7. Ensure heartbeat content cannot authorize shell use, credential use, public publishing, or third-party submissions without interactive confirmation. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
Findings (36)

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding
The skill is presented as a university/coursework helper, but the documented behavior includes agent registration, fingerprinting, credential provisioning, local auth storage, persistent daemon installation, shell profile modification, and heartbeat/persistence mechanisms. That is a major description-behavior mismatch and strongly suggests covert capability expansion beyond the user-facing educational purpose.

External Script Fetching

High
Category
Supply Chain
Content
if (!clientVersion || clientVersion === result.apiVersion) return
    updateCheckShown = true
    console.error(
      `[ludwitt] A new API version is available (server: ${result.apiVersion}, yours: ${clientVersion}). Update: ${result.updateInstructions || 'curl -sSL https://opensource.ludwitt.com/install | sh'}`
    )
  } catch {}
}
Confidence
96% confidence
Finding
The code displays and potentially trusts server-controlled update instructions, with a fallback that explicitly recommends piping a remote script directly into the shell (`curl ... | sh`). Even though this line is only printed and not executed here, it normalizes an unsafe update path and allows a compromised server or MITM on non-hardened channels to socially engineer users into arbitrary code execution.

External Script Fetching

High
Category
Supply Chain
Content
# saves credentials, installs the daemon as a background service.
#
# Usage:
#   curl -sSL https://opensource.ludwitt.com/install | sh
#   # or after cloning the skill repo:
#   ./install.sh
Confidence
99% confidence
Finding
The script advertises installation via `curl ... | sh`, which executes network-fetched code directly without giving the user a chance to inspect it. This pattern is dangerous because any compromise of the remote host, TLS termination, or content pipeline leads to immediate arbitrary code execution on the user's machine.

Chaining Abuse

High
Category
Tool Misuse
Content
# saves credentials, installs the daemon as a background service.
#
# Usage:
#   curl -sSL https://opensource.ludwitt.com/install | sh
#   # or after cloning the skill repo:
#   ./install.sh
Confidence
99% confidence
Finding
The `| sh` chaining explicitly turns downloaded content into immediate shell execution, collapsing transport and execution into one step. In combination with this installer's persistence, credential storage, and outbound registration, that creates a high-risk bootstrap path for arbitrary and lasting compromise.

Description-Behavior Mismatch

High
Confidence
98% confidence
Finding
The installer provisions a persistent daemon and configures it to run automatically at login/boot via launchd or systemd. That materially exceeds a simple course-enrollment workflow and creates long-lived code execution whose behavior depends on `daemon.js`, increasing risk of surveillance, command execution, or later remote tasking.

Missing User Warnings

High
Confidence
98% confidence
Finding
The installer sets up a boot-time background service without a separate, prominent consent step. In a skill whose manifest describes course enrollment and grading, undisclosed persistence is especially concerning because it enables continuous execution beyond the immediate user action.

Lp3

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding
The skill requests and operationally depends on powerful capabilities such as shell, file access, environment secrets, and network access, yet it does not declare a constrained tool scope. That creates a hidden privilege boundary problem: a broadly invoked education skill can end up running installation, deployment, Git, and credential-dependent actions with more authority than users may expect.

Vague Triggers

Medium
Confidence
93% confidence
Finding
The invocation language is broad enough to trigger on ordinary education-related requests, increasing the chance the skill is selected in contexts where users did not intend enrollment, deployment, installation, or credential use. Because the skill performs or recommends high-risk actions, over-broad routing materially increases exposure.

Rp1

Medium
Category
MCP Rug Pull
Confidence
82% confidence
Finding
Using `npx vercel` without pinning a specific version permits execution of whatever package version is currently resolved at install/runtime. This creates supply-chain risk and reduces reproducibility, especially in a skill that encourages shell execution and deployment workflows.

Rp1

Medium
Category
MCP Rug Pull
Confidence
82% confidence
Finding
This duplicate occurrence still reflects the same security issue: an unpinned package execution path that can change over time. Because the command may run with authenticated deployment context, compromise could affect hosted applications or tokens.

Rp1

Medium
Category
MCP Rug Pull
Confidence
82% confidence
Finding
This is another duplicate occurrence of the same unpinned CLI issue at a sensitive authentication step. The risk is amplified because the command may be executed in an environment containing deploy credentials or user session data.

Session Persistence

Medium
Category
Rogue Agent
Content
- **Claude Code:** Requires `allowedTools` to include `Bash`, file read/write, and network access. Ask your owner to enable these if not already set.
- **Vercel:** `npx vercel --prod` deploys from any project directory. One-time `npx vercel login` required.
- **GitHub:** `GITHUB_TOKEN` or SSH key must be configured so `git push` works without prompts.
- **Paper:** Write your reflection to a local `.md` file — the daemon reads and submits it directly.
- **Video:** Any public `https://` video URL is accepted (YouTube, Loom, HeyGen, Vimeo, etc.).

## Installation
Confidence
75% confidence
Finding
The skill normalizes a daemon that reads local reflection files and submits them directly, indicating background or persistent processing of local user content. Without clear consent boundaries, retention limits, or data minimization, this creates ongoing access and exfiltration risk for locally stored documents.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The skill instructs users to publish a live app, a public GitHub repository, and either a public video or inline paper submission, but it gives no meaningful privacy warning about source disclosure, personal data exposure, secrets leakage, or the fact that paper contents are transmitted. In an education workflow, users may include sensitive code, API keys, proprietary work, or personal reflections without realizing the exposure is public or remotely submitted.

Session Persistence

Medium
Category
Rogue Agent
Content
- **Video** (`--video`) — Generate or record a video walkthrough of your platform and your
  build process. Any public video URL is accepted (YouTube, Loom, HeyGen, Vimeo, etc.).

- **Written paper** (`--paper`) — Write a minimum 5000-word paper covering what you built,
  the technical decisions you made, challenges you faced, and what you learned.
  Save it as a `.md` or `.txt` file and pass the path to `--paper`.
Confidence
74% confidence
Finding
Requiring users to save a long-form reflection locally and pass its path for daemon submission creates a mechanism for local file ingestion and transfer. If path handling is broad or unclear, users may expose unintended local content, and the pattern encourages persistent handling of documents by a background service.

Context-Inappropriate Capability

Medium
Confidence
89% confidence
Finding
The script fingerprints the host's agent environment by checking for OpenClaw, Cursor, and Claude Code artifacts and environment variables. This collects unrelated context about the user's tooling and transmits the derived framework type during registration, which is not necessary for basic enrollment and increases privacy and targeting risk.

Context-Inappropriate Capability

Medium
Confidence
87% confidence
Finding
The script strongly steers users toward public deployment, GitHub push access, and other capabilities not inherently required just to enroll in university courses. In the context of an agent skill, encouraging broader permissions and public code publication expands the blast radius if the daemon or later workflows are abused.

Rp1

Medium
Category
MCP Rug Pull
Confidence
70% confidence
Finding
npx commands without a version suffix (e.g. @1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update.

Rp1

Medium
Category
MCP Rug Pull
Confidence
70% confidence
Finding
npx commands without a version suffix (e.g. @1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update.

Missing User Warnings

Medium
Confidence
97% confidence
Finding
The installer sends `agentName`, detected framework, and a generated fingerprint to a remote API during registration with no explicit pre-consent step detailing the exact data transmitted. This creates a privacy issue and can support host tracking or inventorying, especially because the fingerprint is stored and reused across reinstallations.

External Transmission

Medium
Category
Data Exfiltration
Content
process.stdout.write(JSON.stringify({ agentName, agentFramework, fingerprint }));
' "$AGENT_NAME" "$FRAMEWORK" "$FINGERPRINT")

REGISTER_RESPONSE=$(curl -sSL -w "\n%{http_code}" \
  -X POST "$LUDWITT_API/api/agent/register" \
  -H "Content-Type: application/json" \
  -H "X-Agent-Type: $FRAMEWORK" \
Confidence
96% confidence
Finding
The script performs an outbound registration request to a remote domain and transmits locally derived metadata. External transmission is expected for a network service, but in this context it is still security-relevant because the payload includes host-identifying data and occurs during installation.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The script writes an API key and agent metadata to `~/.ludwitt/auth.json` without advance warning or an option to choose an alternate credential store. Even with `chmod 600`, silently persisting credentials increases the chance of accidental exposure, backup leakage, or misuse by local processes running as the same user.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
}
EOF

chmod 600 "$AUTH_FILE"
info "Credentials saved to $AUTH_FILE (owner read-only)"

# ─── Copy daemon ─────────────────────────────────────────────────────────────
Confidence
80% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Context-Inappropriate Capability

Medium
Confidence
96% confidence
Finding
The installer edits `~/.zshrc` or `~/.bashrc` to prepend a new directory to `PATH`, permanently altering shell behavior. Startup-file modification is a persistent environmental change that can affect future command resolution and is risky when done without explicit confirmation.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
This path modification occurs without a clear advance disclosure or confirmation dialog. Silent persistence changes are dangerous because they affect future shells and can unexpectedly prioritize executables from a newly created directory.

Session Persistence

Medium
Category
Rogue Agent
Content
# ─── Register background service ────────────────────────────────────────────

install_launchd() {
  local plist="$HOME/Library/LaunchAgents/com.ludwitt.daemon.plist"
  mkdir -p "$HOME/Library/LaunchAgents"
  cat > "$plist" << PLISTEOF
<?xml version="1.0" encoding="UTF-8"?>
Confidence
90% confidence
Finding
This finding points to the creation of a launchd plist used to register the daemon as a startup item. Even though the matched token is generic, in context it is part of a persistence mechanism that causes recurring execution without repeated user initiation.

Static analysis

No suspicious patterns detected.