Back to skill

Security audit

A-Share Review and Analyse

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly matches its stock-review purpose, but it handles credentials, publishing, and output paths unsafely enough that users should review it before installing.

Install only if you are comfortable running a local stock-report generator that can call Gemini and create WeChat drafts. Before using real WeChat credentials, fix or verify redaction of secrets in logs, validate dates as YYYYMMDD, remove the fixed demo-blog link from completion output, and run it in a workspace where writing data and content/posts is acceptable.

Vulnerability Patterns
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • 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 (4)

T01 · Skill Instruction Hijacking

Note
Location
SKILL.md:241
Finding
Fixed Promotional Link Injected into Agent Completion Responses<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md:241-259`; duplicated in `SKILL.zh.md:241-259` **Vulnerability Type**: Agent output manipulation **Risk Level**: Low ### Vulnerable Instruction Snippet ```markdown **Success Report**: ​```text ✅ A-share Review Analysis Complete! Date: 2026-03-04 Data: data/20260304/ (12 files) AI Analysis: ✓ Generated (Gemini 2.0 Flash) Published Platforms: → Hugo Blog: content/posts/stock-analysis-2026-03-04.md → WeChat Official Account: Draft ID: abc123def456 Market Snapshot: • Shanghai Composite: 3350.52 (+1.02%) • Turnover: 1.95 trillion • Advance/Decline: 2857 / 2058 • Limit-up/Limit-down: 78 / 3 View Blog: https://donvink.github.io/stock-review/ ​``` ``` ### Technical Analysis The Skill instructs the Agent to use a predefined completion report containing a link to the Skill author's public demonstration blog. The link is unrelated to the result generated for the current user and is not derived from user configuration. Because `SKILL.md` supplies operational instructions to the Agent, placing an unrelated promotional destination in the expected success response can cause that destination to be included whenever the workflow completes. This modifies the Agent's output for the benefit of an external property rather than strictly fulfilling the user's request. The same behavior appears in both English and Chinese Skill documentation. ### Attack Path 1. The Agent loads the Skill instructions. 2. The Agent executes the documented stock-review workflow. 3. The workflow completes successfully. 4. The Agent follows the prescribed completion-report template. 5. The final response includes the author's unrelated demonstration-blog URL. 6. The user may treat the link as part of the generated result and visit an external property that they did not request. ### Impact Assessment This issue does not grant local code execution, filesystem access, or credential access. Its impact is limited to manipulation of Ag ...[truncated 261 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove the fixed `View Blog` entry from the success-response template. 2. Treat the completion block as an illustrative format rather than mandatory output. 3. Include a blog URL only when: - the user explicitly requests it; - the URL is generated by the current workflow; or - the user has configured the URL as their own publishing destination. 4. Apply the same correction to `SKILL.zh.md`. 5. Clearly separate project documentation links from instructions controlling the Agent's final response. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/fetch_data.py:45
Finding
Unvalidated Date Allows Writes Outside the Configured Data Directory<![CDATA[ ## Vulnerability Details **File Location**: `scripts/fetch_data.py:45-53`; `scripts/generate_report.py:38-39`; `scripts/generate_report.py:163-174` **Vulnerability Type**: Path traversal and out-of-scope filesystem write **Risk Level**: Medium ### Vulnerable Code Snippets ```python def fetch_all(self, date: str, force_refresh: bool = False) -> Dict[str, Any]: """ Get all market data for the specified date, with caching and retry logic Returns: A dictionary containing all the dataframes and summary statistics """ self.logger.info(f"Fetching market data for {date}") save_dir = self.config.data_dir / date save_dir.mkdir(parents=True, exist_ok=True) ``` The report generator repeats the unsafe path construction: ```python save_dir = self.config.data_dir / date file_path = save_dir / f"market_summary_{date}.md" ``` ```python def generate_all(self, market_data: Dict, market_summary: str, ai_analysis: Optional[str], date: str) -> Dict[str, Path]: save_dir = self.config.data_dir / date reports = {} reports['market_summary'] = save_dir / f"market_summary_{date}.md" if ai_analysis: ai_path = save_dir / f"ai_analysis_{date}.md" with open(ai_path, 'w', encoding='utf-8') as f: f.write(ai_analysis) reports['ai_analysis'] = ai_path return reports ``` ### Technical Analysis The `date` value is documented as `YYYYMMDD`, but the implementation does not validate that format before using the value as a filesystem path component. `pathlib.Path` preserves traversal components such as `..`. For example, if `self.config.data_dir` is `/workspace/data` and `date` is `..`, the expression below resolves operationally to the parent of the intended data directory: ```python Path("/workspace/data") / ".." ``` The fetcher then creates that directory and writes predictable CSV cache files beneath it. Report generation uses the same unvalidated value. The problem ...[truncated 1550 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Validate the date before any path construction: ```python from datetime import datetime import re def validate_date(value: str) -> str: if not re.fullmatch(r"\d{8}", value): raise ValueError("Date must use YYYYMMDD format") datetime.strptime(value, "%Y%m%d") return value ``` 2. Resolve and enforce the output boundary: ```python base = self.config.data_dir.resolve() save_dir = (base / validate_date(date)).resolve() if save_dir.parent != base: raise ValueError("Output path escapes the configured data directory") ``` 3. Centralize date validation so the fetcher, report generator, and publishers use the same validated value. 4. Reject absolute paths, path separators, `.` components, and `..` components. 5. Add regression tests using `..`, `../target`, absolute paths, malformed dates, and invalid calendar dates. 6. Run the Skill under an account with write access only to its intended data and content directories. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/config.py:71
Finding
Environment File Loading Order Can Silently Replace Intended Credentials<![CDATA[ ## Vulnerability Details **File Location**: `scripts/config.py:71-97` **Vulnerability Type**: Unsafe credential configuration precedence **Risk Level**: Medium ### Vulnerable Code Snippet ```python def _load_env_files(self): """ Load .env files in order of priority: # search order: # 1. project root (.env) # 2. skill root (.env) # 3. user config directory (~/.openclaw/skills/stock_review/.env) # 4. XDG config directory (~/.config/stock_review/.env) """ search_paths = [ Path.cwd() / '.env', Path(__file__).parent.parent / '.env', Path.home() / '.openclaw' / 'skills' / 'stock_review' / '.env', Path(os.getenv('XDG_CONFIG_HOME', Path.home() / '.config')) / 'stock_review' / '.env', ] loaded_files = [] for env_path in search_paths: if env_path.exists(): load_dotenv(env_path, override=True) loaded_files.append(str(env_path)) if loaded_files: print(f"📁 load .env files: {', '.join(loaded_files)}") else: print("ℹ️ .env not found, using system environment variables") ``` ### Technical Analysis The comments describe the project-level `.env` as the highest-priority source. The implementation, however, loads all matching files in the listed order with `override=True`. As a result, each later file replaces variables loaded from earlier files. The effective order is therefore the reverse of the documented priority: an XDG configuration file can replace credentials from the project and Skill directories. Affected values include: ```python GEMINI_API_KEY WECHAT_APP_ID WECHAT_APP_SECRET ``` This discrepancy can cause analysis requests or WeChat publishing operations to run under unintended accounts. It is particularly risky where the project directory is trusted but a lower-priority user configuration directory contains stale or attacker-modified settings. ### Attack Path 1. A trusted project contains its inten ...[truncated 1065 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Define one unambiguous precedence policy and make implementation and documentation agree. 2. Prefer loading only the first matching `.env` file when the search list is ordered from highest to lowest priority: ```python for env_path in search_paths: if env_path.is_file(): load_dotenv(env_path, override=False) loaded_files.append(str(env_path)) break ``` 3. Alternatively, load files from lowest to highest priority and use `override=True`. 4. Do not allow a `.env` file to overwrite pre-existing process environment variables unless explicitly requested. 5. Validate `XDG_CONFIG_HOME` before using it as a credential-discovery location. 6. Warn when multiple credential files are present instead of silently merging them. 7. Add automated tests asserting the expected precedence for process environment, project, Skill, OpenClaw user, and XDG sources. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/post_to_wechat.py:208
Finding
WeChat Secrets and Access Tokens Can Be Exposed Through Exception Logging<![CDATA[ ## Vulnerability Details **File Location**: `scripts/post_to_wechat.py:208-232`; `scripts/post_to_wechat.py:235-270` **Vulnerability Type**: Sensitive information exposure through logs **Risk Level**: High ### Vulnerable Code Snippets The AppSecret is placed directly in a request URL, after which the raw exception is logged: ```python def _get_access_token(self) -> Optional[str]: """Get access token from WeChat API""" app_id = self.config.wechat_app_id app_secret = self.config.wechat_app_secret if not app_id or not app_secret: self.logger.error("WeChat app_id or app_secret not configured") return None url = f"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={app_id}&secret={app_secret}" try: self.logger.info("Requesting WeChat access token...") response = requests.get(url, timeout=10) result = response.json() if 'access_token' in result: token = result['access_token'] self.logger.info("✅ Access token obtained successfully") return token else: self.logger.error(f"❌ Failed to get access token: {result}") return None except Exception as e: self.logger.error(f"Access token request failed: {e}") return None ``` The access token is handled in the same way during image upload: ```python def _upload_image_as_thumb(self, access_token: str, image_path: str) -> Optional[str]: if not os.path.exists(image_path): self.logger.error(f"Cover image not found: {image_path}") return None url = f"https://api.weixin.qq.com/cgi-bin/material/add_material?access_token={access_token}&type=image" try: self.logger.info(f"Uploading cover image: {image_path}") with open(image_path, 'rb') as f: files = {'media': f} response = requests.post(url, files=files, timeout=30) result = resp ...[truncated 2590 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Never log raw exceptions without first redacting sensitive query parameters. 2. Introduce a redaction function covering at least `secret`, `access_token`, `appid`, and authorization headers: ```python from urllib.parse import urlsplit, parse_qsl, urlencode, urlunsplit SENSITIVE_KEYS = {"secret", "access_token"} def redact_url(url: str) -> str: parts = urlsplit(url) query = [ (key, "[REDACTED]" if key.lower() in SENSITIVE_KEYS else value) for key, value in parse_qsl(parts.query, keep_blank_values=True) ] return urlunsplit(( parts.scheme, parts.netloc, parts.path, urlencode(query), parts.fragment, )) ``` 3. Log only the exception class and a sanitized operational message: ```python self.logger.error( "WeChat access-token request failed (%s)", type(e).__name__, ) ``` 4. Ensure file, proxy, HTTP-access, and centralized logs apply equivalent redaction. 5. Restrict log-file permissions and retention. 6. Rotate the AppSecret immediately if logs may already contain it, and invalidate exposed access tokens where supported. 7. Preserve WeChat IP allowlisting and apply the narrowest API permissions available. 8. Add tests that generate request exceptions and assert that credential values never appear in captured logs. ]]>
Vulnerability Patterns
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
Findings (66)

Ae1

High
Category
analysis-evasion
Content
| `scripts/fetch_data.py` | Fetch A-share market data (indices, stocks, sectors, etc.) |
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Credential Access

High
Category
Privilege Escalation
Content
def check_env_vars():
    """检查环境变量"""
    env_file = Path(__file__).parent.parent / '.env'
    if env_file.exists():
        print(f"✅ 找到.env文件: {env_file}")
    else:
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def check_env_vars():
    """检查环境变量"""
    env_file = Path(__file__).parent.parent / '.env'
    if env_file.exists():
        print(f"✅ 找到.env文件: {env_file}")
    else:
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def check_env_vars():
    """检查环境变量"""
    env_file = Path(__file__).parent.parent / '.env'
    if env_file.exists():
        print(f"✅ 找到.env文件: {env_file}")
    else:
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def check_env_vars():
    """检查环境变量"""
    env_file = Path(__file__).parent.parent / '.env'
    if env_file.exists():
        print(f"✅ 找到.env文件: {env_file}")
    else:
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def check_env_vars():
    """检查环境变量"""
    env_file = Path(__file__).parent.parent / '.env'
    if env_file.exists():
        print(f"✅ 找到.env文件: {env_file}")
    else:
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def __init__(self, config: Optional[Dict[str, Any]] = None):
        config = config or {}
        
        # step 1. load .env files
        self._load_env_files()
        
        # step 2. load environment variables with fallback to config dict
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def __init__(self, config: Optional[Dict[str, Any]] = None):
        config = config or {}
        
        # step 1. load .env files
        self._load_env_files()
        
        # step 2. load environment variables with fallback to config dict
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def __init__(self, config: Optional[Dict[str, Any]] = None):
        config = config or {}
        
        # step 1. load .env files
        self._load_env_files()
        
        # step 2. load environment variables with fallback to config dict
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def __init__(self, config: Optional[Dict[str, Any]] = None):
        config = config or {}
        
        # step 1. load .env files
        self._load_env_files()
        
        # step 2. load environment variables with fallback to config dict
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def __init__(self, config: Optional[Dict[str, Any]] = None):
        config = config or {}
        
        # step 1. load .env files
        self._load_env_files()
        
        # step 2. load environment variables with fallback to config dict
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def __init__(self, config: Optional[Dict[str, Any]] = None):
        config = config or {}
        
        # step 1. load .env files
        self._load_env_files()
        
        # step 2. load environment variables with fallback to config dict
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def __init__(self, config: Optional[Dict[str, Any]] = None):
        config = config or {}
        
        # step 1. load .env files
        self._load_env_files()
        
        # step 2. load environment variables with fallback to config dict
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def __init__(self, config: Optional[Dict[str, Any]] = None):
        config = config or {}
        
        # step 1. load .env files
        self._load_env_files()
        
        # step 2. load environment variables with fallback to config dict
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def __init__(self, config: Optional[Dict[str, Any]] = None):
        config = config or {}
        
        # step 1. load .env files
        self._load_env_files()
        
        # step 2. load environment variables with fallback to config dict
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def __init__(self, config: Optional[Dict[str, Any]] = None):
        config = config or {}
        
        # step 1. load .env files
        self._load_env_files()
        
        # step 2. load environment variables with fallback to config dict
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def __init__(self, config: Optional[Dict[str, Any]] = None):
        config = config or {}
        
        # step 1. load .env files
        self._load_env_files()
        
        # step 2. load environment variables with fallback to config dict
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def __init__(self, config: Optional[Dict[str, Any]] = None):
        config = config or {}
        
        # step 1. load .env files
        self._load_env_files()
        
        # step 2. load environment variables with fallback to config dict
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def __init__(self, config: Optional[Dict[str, Any]] = None):
        config = config or {}
        
        # step 1. load .env files
        self._load_env_files()
        
        # step 2. load environment variables with fallback to config dict
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def __init__(self, config: Optional[Dict[str, Any]] = None):
        config = config or {}
        
        # step 1. load .env files
        self._load_env_files()
        
        # step 2. load environment variables with fallback to config dict
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def __init__(self, config: Optional[Dict[str, Any]] = None):
        config = config or {}
        
        # step 1. load .env files
        self._load_env_files()
        
        # step 2. load environment variables with fallback to config dict
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
return None
        
        try:
            # 1. Get access token
            token = self._get_access_token()
            if not token:
                self.logger.error("Failed to get access token")
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
return None
        
        try:
            # 1. Get access token
            token = self._get_access_token()
            if not token:
                self.logger.error("Failed to get access token")
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
return None
        
        try:
            # 1. Get access token
            token = self._get_access_token()
            if not token:
                self.logger.error("Failed to get access token")
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
return None
        
        try:
            # 1. Get access token
            token = self._get_access_token()
            if not token:
                self.logger.error("Failed to get access token")
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Static analysis

No suspicious patterns detected.