T09 · Insecure Skill Coding Practices
- Location
scripts/qb.py:41- Finding
API Key Disclosure Through an Unrestricted API Base URL
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
The skill mostly matches its stated purpose, but its API-key handling can expose the key if a custom API server or command-line key is used.
Review before installing. Use this only if you trust the API provider, keep QB_API_BASE on the official HTTPS endpoint unless you fully trust another gateway, prefer QB_API_KEY in the environment over --key, avoid sharing logs or commands containing the key, and choose Word export paths carefully because existing files may be overwritten.
scripts/qb.py:41API Key Disclosure Through an Unrestricted API Base URL
scripts/test_local.sh:19API Key Exposure Through Command-Line Arguments
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
last = None
for attempt in range(3):
try:
with urllib.request.urlopen(req, timeout=timeout) as r:
if raw or out_path:
data = r.read()
if out_path:
The skill invokes a Python script that uses environment variables and performs outbound network requests to a third-party API, but the manifest does not declare any tool scope or allowed tools. This creates an authorization and review gap: an agent may run code with broader-than-expected capabilities, including reading sensitive env vars or making unintended external requests, without explicit policy constraints.
The CLI help text, warnings, and printed output are presented in Chinese throughout the parser and runtime messages, which effectively forces a specific language for users. There is no visible option to select another locale or an explicit statement that the tool is region-specific and intentionally Chinese-only.
This markdown file tells users to configure QB_API_KEY for the skill, which is a sensitive credential, but provides no warning about keeping the key secret or avoiding sharing it in logs, screenshots, or prompts. Under the markdown variant of SQP-2, omission of warnings about privacy- or data-affecting behavior is a valid finding.
This markdown file documents that the script saves the binary DOCX stream to a local file via --out, which is a file-write operation. Under the markdown-specific warning rule, the description should disclose behaviors that affect user data or the local system, but no caution or warning is provided around overwriting or creating local files.
The post() helper serializes payloads and sends them to a remote vendor API, including user-provided query content and identifiers, and may attach an API key header. While the module docstring describes the API client generally, the code path itself provides no confirmation prompt or user-facing disclosure when transmitting potentially sensitive user input to the external service.
When out_path is provided, the code opens the path in write-binary mode and saves remote response data, overwriting any existing file at that location. Although the save is part of the command purpose, there is no pre-write warning, confirmation, or overwrite notice near the file-write operation itself.
Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.
def build_knowledge_payload(knowledgeId, args, page):
p = {"knowledgeId": knowledgeId, "page": page or 1}
for k in ("qtypeId", "paperType", "diff", "gradeId", "year"):
v = getattr(args, k, None)
if v is not None:
p[k] = v
return p
Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.
def cmd_chapter_tree(args, c):
p = {"pharseId": args.pharseId}
for k in ("subjectId", "editionId", "gradeId"):
v = getattr(args, k)
if v:
p[k] = v
d = post(*c, "/api/v1/chapterApi", p)
Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.
def cmd_chapter_tree(args, c):
p = {"pharseId": args.pharseId}
for k in ("subjectId", "editionId", "gradeId"):
v = getattr(args, k)
if v:
p[k] = v
d = post(*c, "/api/v1/chapterApi", p)
Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.
def cmd_chapter_tree(args, c):
p = {"pharseId": args.pharseId}
for k in ("subjectId", "editionId", "gradeId"):
v = getattr(args, k)
if v:
p[k] = v
d = post(*c, "/api/v1/chapterApi", p)
Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.
def cmd_chapter_tree(args, c):
p = {"pharseId": args.pharseId}
for k in ("subjectId", "editionId", "gradeId"):
v = getattr(args, k)
if v:
p[k] = v
d = post(*c, "/api/v1/chapterApi", p)
Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.
def cmd_chapter_tree(args, c):
p = {"pharseId": args.pharseId}
for k in ("subjectId", "editionId", "gradeId"):
v = getattr(args, k)
if v:
p[k] = v
d = post(*c, "/api/v1/chapterApi", p)
Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.
def cmd_chapter_tree(args, c):
p = {"pharseId": args.pharseId}
for k in ("subjectId", "editionId", "gradeId"):
v = getattr(args, k)
if v:
p[k] = v
d = post(*c, "/api/v1/chapterApi", p)