T09 · Insecure Skill Coding Practices
- Location
scripts/portfolio.py:27- Finding
Portfolio Financial Data Is Stored Without Enforced Access Controls or Safe File Replacement
- Content
View full analysis
Vulnerability Details
File Location:
scripts/portfolio.py, lines 27–49
Vulnerability Type: Plaintext sensitive-data storage, insecure file permissions, and unsafe file replacement
Risk Level: Mediumpython PORTFOLIO_FILE = Path.home() / ".clawdbot" / "skills" / "a-stock-analysis" / "portfolio.json" def load_portfolio() -> dict: """加载持仓数据""" if PORTFOLIO_FILE.exists(): with open(PORTFOLIO_FILE, "r", encoding="utf-8") as f: return json.load(f) return {"positions": [], "updated_at": None} def save_portfolio(data: dict): """保存持仓数据""" PORTFOLIO_FILE.parent.mkdir(parents=True, exist_ok=True) data["updated_at"] = datetime.now().isoformat() with open(PORTFOLIO_FILE, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2)Technical Analysis
The portfolio file contains financially sensitive information, including stock identifiers, acquisition costs, quantities, and timestamps. The application serializes this information as unencrypted JSON in the user's home directory.
The directory and file are created without explicit restrictive permissions. Consequently, their effective permissions depend on the process umask and any permissions already assigned to an existing file or directory. A permissive environment may therefore make portfolio information accessible to other local users or processes.
The application also opens the final destination directly with write mode. It does not verify that the destination is a regular file owned by the current user, reject symbolic links, or perform an atomic replacement through a securely created temporary file. Directly truncating and rewriting the destination introduces the possibility of corruption during an interrupted write and may enable link-based redirection if an attacker can modify the containing path.
Attack Path
A confidentiality attack can proceed as follows:
- The victim runs a portfolio command that ...[truncated 1703 chars]
- Remediation
View remediation
Remediation Suggestions
- Create the portfolio directory with owner-only permissions and verify its ownership and type:
python PORTFOLIO_FILE.parent.mkdir(parents=True, exist_ok=True, mode=0o700) os.chmod(PORTFOLIO_FILE.parent, 0o700)-
Reject symbolic links and unexpected file types before reading or replacing the destination. Use
lstat()for validation rather than following links. -
Write through a securely created temporary file in the same directory, enforce mode
0600, flush it, and atomically replace the destination:
python import os import tempfile directory = PORTFOLIO_FILE.parent directory.mkdir(parents=True, exist_ok=True, mode=0o700) os.chmod(directory, 0o700) fd, temporary_path = tempfile.mkstemp( prefix=".portfolio-", suffix=".tmp", dir=directory, ) try: os.fchmod(fd, 0o600) with os.fdopen(fd, "w", encoding="utf-8") as stream: json.dump(data, stream, ensure_ascii=False, indent=2) stream.flush() os.fsync(stream.fileno()) if PORTFOLIO_FILE.is_symlink(): raise RuntimeError("Refusing to replace a symbolic link") os.replace(temporary_path, PORTFOLIO_FILE) os.chmod(PORTFOLIO_FILE, 0o600) finally: if os.path.exists(temporary_path): os.unlink(temporary_path)-
Validate that an existing portfolio file is a regular file owned by the current user before loading it.
-
Consider encrypting the portfolio at rest using an operating-system credential store or a well-reviewed encryption mechanism if confidentiality against local file disclosure is required.
-
Handle malformed or partially written JSON explicitly so file corruption produces a controlled error rather than terminating the command unexpectedly.
