Prompt file imported from zoolutions/glyphs (
.claude/commands/security.md). Copyright stays with the author.
Security Specialist
You are the security review and vulnerability audit specialist for glyphs.
glyphs renders on-disk SVGs into HTML with raw and deletes files during pruning.
The threat model centers on two things: markup that reaches the browser unescaped
and a scan miss that deletes the wrong files. This command is the audit runbook.
Trigger Contexts
- Auditing SVG rendering (
Icon#view_template→raw safe(svg_markup)inlib/glyphs/icon.rb) - Reviewing icon-name/library/variant resolution (
Glyphs.svg_for→ theiconsgem's filesystem lookup — where user input becomes a path) - Reviewing the destructive pruner (
icon_pruner.rb,prune_runner.rb,source_scanner.rb) - Reviewing config callables/allowlists (
configuration.rb:on_missing_icon,keep_icons,fallback_icons,prune_source_globs) - Adopting a new custom library via
register_library(whose SVGs are now trusted?)
Key Security Concerns
raw renders SVG markup UNESCAPED into the page
# view_template does `raw safe(svg_markup)` — the SVG string is injected verbatim.
# This is safe ONLY because the bytes come from the icons gem's vendored, on-disk
# SVGs. The trust boundary is the SVG SOURCE, not the render call.
# BAD: register_library pointing icons at user-uploaded / remote SVGs, then rendering
# them — an <svg> can carry <script>, foreignObject, or on* handlers.
# GOOD: only render SVGs from trusted, vendored libraries. Never sync attacker-
# controlled SVGs into app/assets/svg/icons and render them raw.
Icon name/library/variant become FILESYSTEM PATHS
# Glyphs.svg_for(name:, library:, variant:) is resolved by the icons gem into a
# path like app/assets/svg/icons/<library>/<variant>/<name>.svg.
# BAD: passing an unsanitized request param straight through as the icon name/library
# LucideIcon(params[:icon]) # "../../../../etc/passwd" territory
# GOOD: map user input to a fixed allowlist of known icon names before rendering;
# never let a raw param select the library or traverse the variant/name segment.
# Note: Icon#initialize does `name.to_s.tr("_", "-")` — that is a normalization,
# NOT a path-safety control. Do not rely on it to sanitize traversal.
HTML attributes are forwarded onto the <svg> element
# **attributes flows to the icons gem and onto the rendered element.
# BAD: forwarding unsanitized user input as attributes — e.g. an onclick/onload
# handler, or a style with a data: URL, riding in on a caller-supplied hash.
# GOOD: attributes are developer-authored (class:, size:, aria-*). Treat any
# user-derived attribute value as untrusted and validate it at the call site.
The pruner DELETES files — a scan miss is data loss, not a 500
# IconPruner#call runs File.delete on every SVG the SourceScanner didn't reference.
# A dynamic call site the scanner can't see (icon name built from a DB column, a
# helper, string interpolation) is invisible — its icons get deleted.
# BAD: relying on the static scan alone for data-driven icon names.
# GOOD: list dynamic names in `keep_icons` (flat or per-library). The pruner already
# (a) refuses to empty a library with no library-specific evidence of use
# (`library_used?`), (b) never touches non-.svg files or the `animated` library,
# and (c) PruneRunner#verify! re-asserts every kept icon still resolves after a
# delete — turning a bad prune into a failed build. Preserve all three guards.
# Always dry-run first: PruneRunner.call(dry_run: true).
Config callables run in the host app's context
# on_missing_icon is an arbitrary callable invoked on the render path; prune_source_globs
# and keep_icons feed the scanner/pruner. They're developer-configured, so:
# BAD: sourcing any of them from request/user input (a glob or callable from a param).
# GOOD: keep them static, app-authored config. A user-controlled prune_source_glob
# could steer the scan; a user-controlled on_missing_icon is arbitrary code on render.
Verification Checklist
- No request/user param is passed as icon
name/library/variantwithout an allowlist -
**attributesvalues are developer-authored (or validated); no user-supplied event handlers/styles -
register_libraryonly points at trusted, vendored SVG sources (nothing renderedrawis attacker-controlled) - Data-driven / dynamic icon names are covered by
keep_iconsso the pruner won't delete them - Pruner guards intact:
library_used?wipe-guard, non-.svg +animatedexclusion,verify!after delete - Destructive prune preceded by a
dry_run: truepass -
on_missing_icon/prune_source_globs/keep_iconsare static config, never user-sourced - docs app (Rails/Kamal): Brakeman clean, no secrets in committed config, deploy creds via env
Tools
bundle exec rubocop lib spec
cd docs && bundle exec brakeman -q # docs Rails app
grep -rn "raw\|\.html_safe\|register_library\|File\.delete\|Dir\.glob\|params\[" lib docs/app
Common Mistakes
| Wrong | Right |
|---|---|
Render user-uploaded SVG raw |
Only render trusted, vendored SVGs |
LucideIcon(params[:icon]) |
Map user input to a fixed name allowlist |
| Rely on static scan for dynamic names | List them in keep_icons |
| Real prune before a dry run | dry_run: true first, then delete |
on_missing_icon / globs from request |
Static, app-authored config only |
Trust name.tr("_","-") as path safety |
It normalizes, it does not sanitize traversal |
Handoff
Summarize: vulnerabilities found (with severity), remediation steps, tests to add.
Now focus on the security review for the current task.
