Do not open a public GitHub issue. Email agricidaniel@gmail.com with the subject lipnardo security: <short description>.
Include:
- Affected version / commit SHA
- Steps to reproduce
- Impact (what an attacker can do)
- Suggested remediation if you have one
Expected response: initial acknowledgement within 7 days. Full triage and patch target: 30 days for anything that leads to code execution, credential exfiltration, financial loss (unauthorized API charges), or privilege escalation on the user's machine.
lipnardo is a Claude Code skill that calls the HeyGen REST API to generate avatar videos. It runs locally, holds an API key in either environment or ~/.heygen/config.json, and writes outputs to ~/Documents/lipnardo_videos/. It does not start network services, does not call external APIs other than api.heygen.com, and does not upload generated content elsewhere.
The realistic threat surfaces are:
| Surface | Example risk | Mitigation in place |
|---|---|---|
| API key exfiltration | Key leaked via env dump, logs, or world-readable config | API key never logged. ~/.heygen/config.json permission warned on read. Cost ledger uses os.open() with O_CREAT mode 0600 to prevent race-window exposure. |
| SSRF via download URL | Attacker MITMs HeyGen response and substitutes file:///etc/passwd to exfiltrate local files |
_validate_url() in heygen_client.py rejects non-HTTP(S) schemes and localhost/private hosts before urllib.request.urlopen. |
| Path traversal via API response | Malicious video_id (../../etc/foo) in API response written as filename |
sanitize_id() regex ^[a-zA-Z0-9_.-]+$ applied to all 6 callers that use IDs as filenames. |
| Financial denial-of-wallet | 5,000-row CSV silently spends $20K on first run | --max-batch 500 default cap with explicit override. Cost warning logged before submission. --max-concurrent bounded to 1-10. --max-retries bounded to 0-10. |
| Negative-cost ledger fraud | --duration -60 produces negative cost, hiding real spend |
_lookup_cost() rejects negative duration. |
| Multipart filename injection | Filename with " or CRLF breaks Content-Disposition header in upload |
asset_manager.py strips ", \, CR, LF before building multipart body. |
| Unbounded retry credit burn | max_retries=999 with exponential backoff = hours of API hammering |
Bounded 0-10. |
| Concurrent ledger corruption | Two processes writing to costs.json simultaneously lose entries |
Atomic write via tmp + os.replace(). |
- Vulnerabilities in HeyGen's API itself — report to HeyGen at security@heygen.com.
- Vulnerabilities in Python's stdlib (
urllib,ssl,json) — report upstream. - Vulnerabilities in Claude Code itself — report to Anthropic.
- Generated video content licensing or commercial-use questions — consult HeyGen's terms.
- Charges incurred from legitimate use of the API — Lipnardo does not refund usage fees.
Documented in git history for transparency. All fixes verified with behavioral tests, not just code-existence checks.
| ID | Severity | Issue | Fix |
|---|---|---|---|
| VULN-001 | HIGH | SSRF via file:// scheme in download_file() URL parameter |
_validate_url() rejects non-HTTP(S) and localhost |
| VULN-002 | HIGH | No batch size cap — 5000-row CSV could spend $20K silently | --max-batch 500 default + cost warning + bounds on --max-concurrent (1-10) and --max-retries (0-10) |
| VULN-003 | MEDIUM | API-returned video_id used unsanitized in local filenames (path traversal via MITM) |
sanitize_id() regex applied at 6 callsites |
| VULN-004 | MEDIUM | ~/.heygen/ directory + config.json created with default umask (world-readable) |
os.open() with O_CREAT mode 0600 for ledger; permission warning on config read |
| VULN-005 | MEDIUM | Negative --duration produced negative cost (ledger fraud loophole) |
Validation in _lookup_cost() |
| VULN-006 | MEDIUM | max_concurrent=0 caused silent batch no-op (data loss illusion) |
Bounds check 1-10 |
| VULN-007 | MEDIUM | Multipart filename in Content-Disposition not escaped |
Strip ", \, CR, LF before header construction |
| VULN-008 | LOW | Cost ledger lacked atomic write (concurrent loss) | tmp + os.replace() pattern |
| API-001 | HIGH | Translation --mode quality was silently ignored by v2 endpoint (users charged 2x for "speed" mode) |
Upgraded to /v3/video-translations with explicit speed/precision mapping |
The 8-agent cybersecurity audit (OWASP Top 10:2025, CWE Top 25:2024, MITRE ATT&CK) scored v1.0 at 78/100 before fixes. All HIGH and MEDIUM findings remediated. The full audit report is in the repository commit history.