Scan PatternFly React code for security anti-patterns — XSS via dangerouslySetInnerHTML, unsanitized user input in tooltips/labels, and insecure href patterns. Use when reviewing PF code for security vulnerabilities or auditing user-controlled content in PF components.
Installs into .claude/skills of the current project.
Are you the author of Pf Security Scan?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/rh-uxd-pf-security-scan)
---
name: pf-security-scan
description: Scan PatternFly React code for security anti-patterns — XSS via dangerouslySetInnerHTML, unsanitized user input in tooltips/labels, and insecure href patterns. Use when reviewing PF code for security vulnerabilities or auditing user-controlled content in PF components.
disable-model-invocation: true
---
# PF Security Scan
Audit PatternFly React usage for security anti-patterns that turn UI components into XSS or open-redirect vectors. This skill focuses on **PF-specific hotspots** — not general app security (auth, CSRF, secrets).
## PatternFly API documentation
When unfamiliar with a component's props or link-rendering behavior, consult available PatternFly documentation (component docs, design guidelines, or local references). This skill defines **what to flag**; docs fill in component-specific details. Ask the user for confirmation before any external documentation lookup that would transmit source context. Without documentation available, rely on the anti-pattern rules and component checklist in this skill.
## How to run
1. Ask the user which directory or files to scan. Default to the current working directory.
2. Limit scope to files importing from `@patternfly/react-core`, `@patternfly/react-table`, `@patternfly/react-component-groups`, or `@patternfly/react-templates`.
3. Run targeted searches (see [references/anti-patterns.md](references/anti-patterns.md) for grep patterns), then read flagged files for context.
4. Trace user-controlled data: API responses, URL params, query strings, form input, localStorage, WebSocket payloads, and props passed from parent components.
5. Report findings grouped by file with line numbers. End with a summary count.
## Anti-patterns to detect
### 1. XSS via `dangerouslySetInnerHTML`
| Severity | Flag when |
|----------|-----------|
| **CRITICAL** | `dangerouslySetInnerHTML` / `__html` with unsanitized or untrusted non-constant HTML (no recognized sanitizer: DOMPurify, sanitize-html, or project-approved equivalent) |
| **WARN** | `dangerouslySetInnerHTML` with sanitization present but unverified config — confirm the sanitizer allows only safe tags |
Also flag PF components that accept HTML strings when fed unsanitized data:
- `innerHTML`-style props or render callbacks that inject raw HTML
- Rich-text editors or markdown renderers whose output flows into PF `Text`, `Title`, `Alert`, or table cells without sanitization
### 2. Unsanitized user input in tooltips, labels, and text surfaces
User-controlled strings rendered in PF components are XSS vectors when the data can contain HTML or script payloads.
| Severity | Component / prop | Flag when |
|----------|------------------|-----------|
| **CRITICAL** | `Tooltip` / `Popover` — `content`, `bodyContent`, `headerContent` | Value from user/API state without sanitization |
| **CRITICAL** | `Label`, `Badge`, `Chip`, `ChipGroup` — children or `text` | User-controlled value rendered directly |
| **HIGH** | `Alert` — `title`, `titleHeadingLevel`, children | Message from API/error payload without sanitization |
| **HIGH** | `Title`, `Text`, `Content` — children | User or CMS content interpolated without escaping |
| **HIGH** | `FormGroup` — `label`, `HelperText` — children | Label or helper text from user input |
| **HIGH** | `EmptyState` — `titleText`, `body` | Dynamic content from external source |
| **HIGH** | `DescriptionListDescription`, `DataListCell`, `Td` | Cell/description values from API without sanitization |
| **WARN** | `MenuItem`, `DropdownItem`, `SelectOption` — children | Display text from user data (lower risk if React text nodes, but still audit source) |
**Safe patterns** (do not flag):
- React text children (`<Label>{userName}</Label>`) — React escapes by default
- Values passed through a project-approved sanitizer at the boundary
- Compile-time string literals
**Unsafe patterns** (always flag):
- `dangerouslySetInnerHTML` in or near PF text surfaces
- `innerHTML` assignment on DOM nodes inside PF component refs
- Rendering markdown/HTML from user input without sanitization pipeline
### 3. Insecure URL / href patterns
PF components frequently render links via `Button`, `NavItem`, `BreadcrumbItem`, `DropdownItem`, `MenuItem`, and standalone `<a>` inside PF layouts.
| Severity | Flag when |
|----------|-----------|
| **CRITICAL** | `href` or `to` set to `javascript:` URL (literal or constructed) |
| **CRITICAL** | `href` built from unsanitized user input without URL validation |
| **HIGH** | `href` using `data:` scheme with non-image MIME types |
| **HIGH** | Dynamic `href` from API/params with no allowlist or `URL` constructor validation |
| **WARN** | `target="_blank"` without `rel="noopener noreferrer"` on external links |
| **WARN** | `Button component="a"` with dynamic `href` — same rules as `<a>` |
**PF-specific link surfaces to check:**
| Component | Props |
|-----------|-------|
| `Button` | `href`, `component="a"` |
| `NavItem` / `NavLink` | `to`, `href` |
| `BreadcrumbItem` | `to`, `href` |
| `DropdownItem` | `href`, `to` |
| `MenuItem` | `to`, `href` |
| `JumpLinksItem` | `href` |
| `ExternalLink` (if used) | `href` |
**Safe patterns:**
```tsx
// Allowlisted origin
const safeUrl = allowedOrigins.includes(new URL(userUrl).origin) ? userUrl : '#';
// React Router internal navigation only
<NavItem to={`/users/${userId}`}>...</NavItem>
// External link with tab-nabbing protection
<Button component="a" href={trustedUrl} target="_blank" rel="noopener noreferrer">
```
**Unsafe patterns:**
```tsx
// javascript: injection
<Button component="a" href={userSuppliedUrl}>...</Button>
// Unvalidated redirect
<DropdownItem href={searchParams.get('redirect')}>...</DropdownItem>
// String concat into href from API
<BreadcrumbItem href={`/docs/${apiResponse.path}`}>...</BreadcrumbItem>
```
## Report format
For each finding:
```text
[CRITICAL|HIGH|WARN] file/path.tsx:42 - Unsanitized API data in Tooltip content
Found: <Tooltip content={apiResponse.description}>
Risk: XSS — attacker-controlled HTML in tooltip popper
Fix: Sanitize with DOMPurify before render, or render as React text child if HTML is not required
```
End with:
```text
Scanned: 18 files
Critical: 2 (across 1 file)
High: 3 (across 2 files)
Warnings: 1 (across 1 file)
```
Group findings by anti-pattern category (XSS, unsanitized input, insecure URLs) so patterns are visible.
## Applying fixes
1. **Prefer React text nodes** over HTML rendering when formatting is not required.
2. **Sanitize at the boundary** — one approved sanitizer at the data ingress point, not scattered per component.
3. **Validate URLs** — parse with `new URL()`, allowlist origins or paths, reject `javascript:`, `data:`, and protocol-relative `//` from untrusted input.
4. **Add `rel="noopener noreferrer"`** to every `target="_blank"` link.
5. Do not weaken sanitizer configs to "make it work" — flag and ask the user if legitimate HTML is required.
Only apply fixes when the remediation is unambiguous. If a finding needs a product decision (e.g., rich-text support), report it and ask.
## Additional resources
- Grep patterns and component checklist: [references/anti-patterns.md](references/anti-patterns.md)
Files in this skill
SKILL.md7.1 KB
eval/cases/insecure-redirect-href/RedirectMenu.tsx479 B
eval/cases/insecure-redirect-href/annotations.yaml139 B
eval/cases/insecure-redirect-href/input.yaml140 B
eval/cases/safe-react-text/SafeLabels.tsx428 B
eval/cases/safe-react-text/annotations.yaml25 B
eval/cases/safe-react-text/input.yaml93 B
eval/cases/unsanitized-tooltip-xss/UserProfileCard.tsx560 B
eval/cases/unsanitized-tooltip-xss/annotations.yaml140 B
eval/cases/unsanitized-tooltip-xss/input.yaml145 B