T09 · Insecure Skill Coding Practices
- Location
references/config.yaml:1- Finding
GitHub PAT Stored in Plaintext and Exposed Through Process Arguments
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
The skill matches a sprint release-notes workflow, but it handles broad GitHub credentials insecurely and can publish releases to repositories chosen from project-board data without a clear approval boundary.
Review carefully before installing. Use a fine-grained GitHub token limited to the exact approved repositories, do not store the token in the skill config or pass it on the command line, run dry-run first, and require a preflight list of target repositories before any release or comment is published. Treat contributor scoring and any notification webhook as separate opt-in behavior.
references/config.yaml:1GitHub PAT Stored in Plaintext and Exposed Through Process Arguments
scripts/generate_release_notes.py:646Project-Controlled Repository Names Determine Authenticated Release and Tag Targets
A second description/behavior mismatch indicates the skill may not actually do the per-repo publish flow it promises, instead relying on hardcoded or dry-run behavior. While this is partly an integrity/reliability issue rather than direct exploitation, security-relevant automation that silently targets the wrong repo or does not update expected releases can cause unauthorized changes, confusion, and unsafe operator assumptions.
A second description/behavior mismatch indicates the skill may not actually do the per-repo publish flow it promises, instead relying on hardcoded or dry-run behavior. While this is partly an integrity/reliability issue rather than direct exploitation, security-relevant automation that silently targets the wrong repo or does not update expected releases can cause unauthorized changes, confusion, and unsafe operator assumptions.
Contributor scoring and award selection are unjustified by the stated release-note purpose and require additional collection of reviews, code volume, and inferred difficulty metrics. This creates an unnecessary people-analytics function that can misuse repository data, generate sensitive HR-like judgments, and surprise users who only intended content publication.
The reference material describes behavior that diverges from the declared skill purpose and publication method. In a skill that is supposed to publish GitHub Release descriptions only, embedding repository file-write guidance creates a path for the agent to modify repository contents unexpectedly, which can lead to unauthorized commits, workflow abuse, or policy violations.
The documented write path directly contradicts the manifest by instructing creation or update of repository files on the target branch. In context, this is more dangerous because the skill is expected to publish release notes as GitHub Release descriptions, so a user could invoke the skill expecting non-destructive metadata updates while the implementation instead performs source-repository writes.
The skill requires and directly consumes a GitHub PAT, which is a high-value credential capable of repository reads and writes depending on scope. Combined with command-line entry and automatic publishing, compromise of this token could let an attacker create or alter releases, read project metadata, and potentially access broader GitHub resources.
def main():
parser = argparse.ArgumentParser(description="Generate sprint release notes from GitHub Project Board")
parser.add_argument("--board-url", required=True, help="GitHub Project Board URL")
parser.add_argument("--pat", required=True, help="GitHub Personal Access Token")
parser.add_argument("--release-repo", required=True, help="Repo to publish release notes (owner/repo)")
parser.add_argument("--sprint-id", default=None, help="Specific sprint iteration ID")
parser.add_argument("--output-dir", default=".", help="Directory to save local copy")
The skill instructs network access to GitHub APIs and publication of releases/comments, but it declares no explicit tool scope or permission boundaries. In an agent environment, this means the runtime may invoke broader capabilities than users expect, increasing the chance of unintended outbound requests or repository modifications with a supplied PAT.
The description does not prominently warn that the skill can create or update GitHub Releases across multiple repositories and optionally post comments. Missing an upfront warning is dangerous because users may provide a PAT and project URL without realizing the skill performs cross-repo write operations, raising the likelihood of accidental mass modification.
The trigger conditions are very broad and can activate the skill for generic phrases like 'generate release notes' or sprint summaries. Because the skill can publish releases and post issue comments, overbroad invocation increases the risk of unintended write actions on GitHub repositories when the user may have only wanted advice or draft text.
The skill expands from release-note publishing into contributor performance scoring and recognition generation, which is a materially different and more sensitive use of repository data. This enlarges data processing scope without clear user consent and can expose personnel analytics or bias-inducing outputs unrelated to the operational task.
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
Example (curl; use the user’s PAT and API version header as appropriate):
curl -L -X PATCH \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "X-GitHub-Api-Version: 2026-03-10" \
The configuration exposes an undeclared outbound integration channel via a Discord webhook that is not described in the skill metadata. Hidden or undocumented egress paths are dangerous because they can be used to exfiltrate release data, repository metadata, or other sensitive content outside the expected GitHub-only workflow, reducing operator visibility and consent.
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
# For GraphQL
curl -H "Authorization: bearer {PAT_TOKEN}" \
-H "Content-Type: application/json" \
-X POST https://api.github.com/graphql \
-d '{"query": "..."}'
The skill includes logic for scanning repository contents and PR file diffs to detect documentation changes, which exceeds what is needed to generate sprint release notes from project board items. This unnecessary expansion of scope increases data access and processing of repository content, creating avoidable privacy and least-privilege risks if the agent inspects sensitive files or unrelated materials.
Counting PR reviews per engineer is unrelated to generating sprint release notes and broadens the skill into employee activity analysis. That creates unnecessary collection and processing of personnel-related contribution data, which may expose behavioral metrics or be repurposed beyond the user's stated task.
The reference documents a direct repository write operation to the main branch without warning, confirmation, or safety constraints. Even if writes were intended, omission of user-facing warnings and approval checkpoints raises the risk of silent modification of production repositories and accidental persistence of generated content.
The skill claims to generate and publish sprint release notes, but it also evaluates contributors and assigns labels like 'Lead Engineer' and 'MVP'. That hidden expansion of scope processes personnel-performance data that users may not expect, creating privacy, trust, and governance risk especially in enterprise environments.
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
# Configuration & Constants
# ─────────────────────────────────────────────
GRAPHQL_URL = "https://api.github.com/graphql"
REST_BASE = "https://api.github.com"
FEATURE_LABELS = {"feature", "enhancement", "user-facing", "feat", "story"}
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
# Configuration & Constants
# ─────────────────────────────────────────────
GRAPHQL_URL = "https://api.github.com/graphql"
REST_BASE = "https://api.github.com"
FEATURE_LABELS = {"feature", "enhancement", "user-facing", "feat", "story"}
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
# Configuration & Constants
# ─────────────────────────────────────────────
GRAPHQL_URL = "https://api.github.com/graphql"
REST_BASE = "https://api.github.com"
FEATURE_LABELS = {"feature", "enhancement", "user-facing", "feat", "story"}
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
# Configuration & Constants
# ─────────────────────────────────────────────
GRAPHQL_URL = "https://api.github.com/graphql"
REST_BASE = "https://api.github.com"
FEATURE_LABELS = {"feature", "enhancement", "user-facing", "feat", "story"}
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
# Configuration & Constants
# ─────────────────────────────────────────────
GRAPHQL_URL = "https://api.github.com/graphql"
REST_BASE = "https://api.github.com"
FEATURE_LABELS = {"feature", "enhancement", "user-facing", "feat", "story"}
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
if variables:
payload["variables"] = variables
for attempt in range(3):
resp = requests.post(GRAPHQL_URL, headers=self.graphql_headers, json=payload)
if resp.status_code == 403 and "rate limit" in resp.text.lower():
reset_time = int(resp.headers.get("X-RateLimit-Reset", time.time() + 60))
wait = max(reset_time - int(time.time()), 10)
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
def rest_put(self, path, data):
"""PUT request to REST API."""
url = f"{REST_BASE}{path}" if path.startswith("/") else path
resp = requests.put(url, headers=self.rest_headers, json=data)
resp.raise_for_status()
return resp.json()
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.
def rest_put(self, path, data):
"""PUT request to REST API."""
url = f"{REST_BASE}{path}" if path.startswith("/") else path
resp = requests.put(url, headers=self.rest_headers, json=data)
resp.raise_for_status()
return resp.json()
No suspicious patterns detected.