hermes - 💡(How to fix) Fix security: skills_guard._resolve_trust_level prefix-match grants trusted tier to attacker-controlled identifiers

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

tools/skills_guard.py:905-908 uses startswith for trust-tier lookup where the matching set is intended as exact identifiers, opening two escalation paths to the trusted and builtin tiers.

Root Cause

AC:H because both paths require attacker control over a specific GitHub identifier (org membership or claiming official). UI:R because the user has to install the malicious skill.

Fix Action

Fix / Workaround

Path 2 — builtin:

Either lock "official" upstream (GitHub username reservation),

or namespace builtin under a path that an attacker can't squat

(e.g. hermes-builtin/, prefixed by repo-owner constraint).


Happy to open a PR for the `startswith → "==" or startswith + "/"`
half if useful; the `official/` namespace question feels like it
needs a maintainer decision before patching.

Code Example

# skills_guard.py:905-908
for trusted in TRUSTED_REPOS:
    if normalized_source.startswith(trusted) or normalized_source == trusted:
        return "trusted"

---

"openai/skills-evil"startswith("openai/skills") = True  → trusted
"anthropics/skills-foo"startswith("anthropics/skills")     → trusted
"huggingface/skills-bar"startswith("huggingface/skills")    → trusted

---

if normalized_source == "official" or normalized_source.startswith("official/"):
    return "builtin"

---

# Path 1 — trust_level:
for trusted in TRUSTED_REPOS:
    if normalized_source == trusted or normalized_source.startswith(trusted + "/"):
        return "trusted"

# Path 2 — builtin:
# Either lock "official" upstream (GitHub username reservation),
# or namespace builtin under a path that an attacker can't squat
# (e.g. hermes-builtin/, prefixed by repo-owner constraint).
RAW_BUFFERClick to expand / collapse

Summary

tools/skills_guard.py:905-908 uses startswith for trust-tier lookup where the matching set is intended as exact identifiers, opening two escalation paths to the trusted and builtin tiers.

Affected code

# skills_guard.py:905-908
for trusted in TRUSTED_REPOS:
    if normalized_source.startswith(trusted) or normalized_source == trusted:
        return "trusted"

TRUSTED_REPOS = {"openai/skills", "anthropics/skills", "huggingface/skills"}

Compare with GitHubSource.trust_level_for() (skills_hub.py:357-364) which correctly does owner/repo in TRUSTED_REPOS (exact set membership).

Escalation paths

1. trusted-tier via sibling repo on the same org

"openai/skills-evil"      → startswith("openai/skills") = True  → trusted
"anthropics/skills-foo"   → startswith("anthropics/skills")     → trusted
"huggingface/skills-bar"  → startswith("huggingface/skills")    → trusted

Practical exploitability is gated on the attacker registering a repo on OpenAI / Anthropic / HuggingFace's GitHub org — not realistic.

2. builtin-tier via "official" username

Line 902 (separate code path):

if normalized_source == "official" or normalized_source.startswith("official/"):
    return "builtin"

INSTALL_POLICY["builtin"] = ("allow", "allow", "allow") — every scan verdict (including dangerous) is allowed.

Currently the GitHub username official is not registered. An attacker who claims it can publish official/any-repo and have any skill identifier under it bypass the install-time scan entirely.

I'd suggest either claiming the official GitHub username defensively, or replacing the prefix check with an exact-tier membership check (mirroring GitHubSource.trust_level_for()).

Suggested fix

# Path 1 — trust_level:
for trusted in TRUSTED_REPOS:
    if normalized_source == trusted or normalized_source.startswith(trusted + "/"):
        return "trusted"

# Path 2 — builtin:
# Either lock "official" upstream (GitHub username reservation),
# or namespace builtin under a path that an attacker can't squat
# (e.g. hermes-builtin/, prefixed by repo-owner constraint).

CVSS estimate

AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H8.3 (HIGH)

AC:H because both paths require attacker control over a specific GitHub identifier (org membership or claiming official). UI:R because the user has to install the malicious skill.

Relationship to recent merges

This is in the same trust-boundary family as:

  • #30715 — skills traversal guard + hash symmetry
  • #30865 — skills_guard medium/low = safe (fail-closed verdict policy)

Happy to open a PR for the startswith → "==" or startswith + "/" half if useful; the official/ namespace question feels like it needs a maintainer decision before patching.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - 💡(How to fix) Fix security: skills_guard._resolve_trust_level prefix-match grants trusted tier to attacker-controlled identifiers