diff --git a/.agents/skills/security-review/LICENSE b/.agents/skills/security-review/LICENSE new file mode 100644 index 0000000000..45d290bd53 --- /dev/null +++ b/.agents/skills/security-review/LICENSE @@ -0,0 +1,22 @@ +The reference material in this skill is derived from the OWASP Cheat Sheet Series. + +Source: https://cheatsheetseries.owasp.org/ +OWASP Foundation: https://owasp.org/ + +Original content is licensed under: + +Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) +https://creativecommons.org/licenses/by-sa/4.0/ + +You are free to: +- Share — copy and redistribute the material in any medium or format +- Adapt — remix, transform, and build upon the material for any purpose, + even commercially + +Under the following terms: +- Attribution — You must give appropriate credit, provide a link to the + license, and indicate if changes were made. +- ShareAlike — If you remix, transform, or build upon the material, you + must distribute your contributions under the same license as the original. + +Full license text: https://creativecommons.org/licenses/by-sa/4.0/legalcode diff --git a/.agents/skills/security-review/SKILL.md b/.agents/skills/security-review/SKILL.md new file mode 100644 index 0000000000..6a85366167 --- /dev/null +++ b/.agents/skills/security-review/SKILL.md @@ -0,0 +1,312 @@ +--- +name: security-review +description: Security code review for vulnerabilities. Use when asked to "security review", "find vulnerabilities", "check for security issues", "audit security", "OWASP review", or review code for injection, XSS, authentication, authorization, cryptography issues. Provides systematic review with confidence-based reporting. +allowed-tools: Read, Grep, Glob, Bash, Task +license: LICENSE +--- + + + +# Security Review Skill + +Identify exploitable security vulnerabilities in code. Report only **HIGH CONFIDENCE** findings—clear vulnerable patterns with attacker-controlled input. + +## Scope: Research vs. Reporting + +**CRITICAL DISTINCTION:** + +- **Report on**: Only the specific file, diff, or code provided by the user +- **Research**: The ENTIRE codebase to build confidence before reporting + +Before flagging any issue, you MUST research the codebase to understand: +- Where does this input actually come from? (Trace data flow) +- Is there validation/sanitization elsewhere? +- How is this configured? (Check settings, config files, middleware) +- What framework protections exist? + +**Do NOT report issues based solely on pattern matching.** Investigate first, then report only what you're confident is exploitable. + +## Confidence Levels + +| Level | Criteria | Action | +|-------|----------|--------| +| **HIGH** | Vulnerable pattern + attacker-controlled input confirmed | **Report** with severity | +| **MEDIUM** | Vulnerable pattern, input source unclear | **Note** as "Needs verification" | +| **LOW** | Theoretical, best practice, defense-in-depth | **Do not report** | + +## Do Not Flag + +### General Rules +- Test files (unless explicitly reviewing test security) +- Dead code, commented code, documentation strings +- Patterns using **constants** or **server-controlled configuration** +- Code paths that require prior authentication to reach (note the auth requirement instead) + +### Server-Controlled Values (NOT Attacker-Controlled) + +These are configured by operators, not controlled by attackers: + +| Source | Example | Why It's Safe | +|--------|---------|---------------| +| Django settings | `settings.API_URL`, `settings.ALLOWED_HOSTS` | Set via config/env at deployment | +| Environment variables | `os.environ.get('DATABASE_URL')` | Deployment configuration | +| Config files | `config.yaml`, `app.config['KEY']` | Server-side files | +| Framework constants | `django.conf.settings.*` | Not user-modifiable | +| Hardcoded values | `BASE_URL = "https://api.internal"` | Compile-time constants | + +**SSRF Example - NOT a vulnerability:** +```python +# SAFE: URL comes from Django settings (server-controlled) +response = requests.get(f"{settings.SEER_AUTOFIX_URL}{path}") +``` + +**SSRF Example - IS a vulnerability:** +```python +# VULNERABLE: URL comes from request (attacker-controlled) +response = requests.get(request.GET.get('url')) +``` + +### Framework-Mitigated Patterns +Check language guides before flagging. Common false positives: + +| Pattern | Why It's Usually Safe | +|---------|----------------------| +| Django `{{ variable }}` | Auto-escaped by default | +| React `{variable}` | Auto-escaped by default | +| Vue `{{ variable }}` | Auto-escaped by default | +| `User.objects.filter(id=input)` | ORM parameterizes queries | +| `cursor.execute("...%s", (input,))` | Parameterized query | +| `innerHTML = "Loading..."` | Constant string, no user input | + +**Only flag these when:** +- Django: `{{ var|safe }}`, `{% autoescape off %}`, `mark_safe(user_input)` +- React: `dangerouslySetInnerHTML={{__html: userInput}}` +- Vue: `v-html="userInput"` +- ORM: `.raw()`, `.extra()`, `RawSQL()` with string interpolation + +## Review Process + +### 1. Detect Context + +What type of code am I reviewing? + +| Code Type | Load These References | +|-----------|----------------------| +| API endpoints, routes | `authorization.md`, `authentication.md`, `injection.md` | +| Frontend, templates | `xss.md`, `csrf.md` | +| File handling, uploads | `file-security.md` | +| Crypto, secrets, tokens | `cryptography.md`, `data-protection.md` | +| Data serialization | `deserialization.md` | +| External requests | `ssrf.md` | +| Business workflows | `business-logic.md` | +| GraphQL, REST design | `api-security.md` | +| Config, headers, CORS | `misconfiguration.md` | +| CI/CD, dependencies | `supply-chain.md` | +| Error handling | `error-handling.md` | +| Audit, logging | `logging.md` | + +### 2. Load Language Guide + +Based on file extension or imports: + +| Indicators | Guide | +|------------|-------| +| `.py`, `django`, `flask`, `fastapi` | `languages/python.md` | +| `.js`, `.ts`, `express`, `react`, `vue`, `next` | `languages/javascript.md` | +| `.go`, `go.mod` | `languages/go.md` | +| `.rs`, `Cargo.toml` | `languages/rust.md` | +| `.java`, `spring`, `@Controller` | `languages/java.md` | + +### 3. Load Infrastructure Guide (if applicable) + +| File Type | Guide | +|-----------|-------| +| `Dockerfile`, `.dockerignore` | `infrastructure/docker.md` | +| K8s manifests, Helm charts | `infrastructure/kubernetes.md` | +| `.tf`, Terraform | `infrastructure/terraform.md` | +| GitHub Actions, `.gitlab-ci.yml` | `infrastructure/ci-cd.md` | +| AWS/GCP/Azure configs, IAM | `infrastructure/cloud.md` | + +### 4. Research Before Flagging + +**For each potential issue, research the codebase to build confidence:** + +- Where does this value actually come from? Trace the data flow. +- Is it configured at deployment (settings, env vars) or from user input? +- Is there validation, sanitization, or allowlisting elsewhere? +- What framework protections apply? + +Only report issues where you have HIGH confidence after understanding the broader context. + +### 5. Verify Exploitability + +For each potential finding, confirm: + +**Is the input attacker-controlled?** + +| Attacker-Controlled (Investigate) | Server-Controlled (Usually Safe) | +|-----------------------------------|----------------------------------| +| `request.GET`, `request.POST`, `request.args` | `settings.X`, `app.config['X']` | +| `request.json`, `request.data`, `request.body` | `os.environ.get('X')` | +| `request.headers` (most headers) | Hardcoded constants | +| `request.cookies` (unsigned) | Internal service URLs from config | +| URL path segments: `/users//` | Database content from admin/system | +| File uploads (content and names) | Signed session data | +| Database content from other users | Framework settings | +| WebSocket messages | | + +**Does the framework mitigate this?** +- Check language guide for auto-escaping, parameterization +- Check for middleware/decorators that sanitize + +**Is there validation upstream?** +- Input validation before this code +- Sanitization libraries (DOMPurify, bleach, etc.) + +### 6. Report HIGH Confidence Only + +Skip theoretical issues. Report only what you've confirmed is exploitable after research. + +--- + +## Severity Classification + +| Severity | Impact | Examples | +|----------|--------|----------| +| **Critical** | Direct exploit, severe impact, no auth required | RCE, SQL injection to data, auth bypass, hardcoded secrets | +| **High** | Exploitable with conditions, significant impact | Stored XSS, SSRF to metadata, IDOR to sensitive data | +| **Medium** | Specific conditions required, moderate impact | Reflected XSS, CSRF on state-changing actions, path traversal | +| **Low** | Defense-in-depth, minimal direct impact | Missing headers, verbose errors, weak algorithms in non-critical context | + +--- + +## Quick Patterns Reference + +### Always Flag (Critical) +``` +eval(user_input) # Any language +exec(user_input) # Any language +pickle.loads(user_data) # Python +yaml.load(user_data) # Python (not safe_load) +unserialize($user_data) # PHP +deserialize(user_data) # Java ObjectInputStream +shell=True + user_input # Python subprocess +child_process.exec(user) # Node.js +``` + +### Always Flag (High) +``` +innerHTML = userInput # DOM XSS +dangerouslySetInnerHTML={user} # React XSS +v-html="userInput" # Vue XSS +f"SELECT * FROM x WHERE {user}" # SQL injection +`SELECT * FROM x WHERE ${user}` # SQL injection +os.system(f"cmd {user_input}") # Command injection +``` + +### Always Flag (Secrets) +``` +password = "hardcoded" +api_key = "sk-..." +AWS_SECRET_ACCESS_KEY = "..." +private_key = "-----BEGIN" +``` + +### Check Context First (MUST Investigate Before Flagging) +``` +# SSRF - ONLY if URL is from user input, NOT from settings/config +requests.get(request.GET['url']) # FLAG: User-controlled URL +requests.get(settings.API_URL) # SAFE: Server-controlled config +requests.get(f"{settings.BASE}/{x}") # CHECK: Is 'x' user input? + +# Path traversal - ONLY if path is from user input +open(request.GET['file']) # FLAG: User-controlled path +open(settings.LOG_PATH) # SAFE: Server-controlled config +open(f"{BASE_DIR}/{filename}") # CHECK: Is 'filename' user input? + +# Open redirect - ONLY if URL is from user input +redirect(request.GET['next']) # FLAG: User-controlled redirect +redirect(settings.LOGIN_URL) # SAFE: Server-controlled config + +# Weak crypto - ONLY if used for security purposes +hashlib.md5(file_content) # SAFE: File checksums, caching +hashlib.md5(password) # FLAG: Password hashing +random.random() # SAFE: Non-security uses (UI, sampling) +random.random() for token # FLAG: Security tokens need secrets module +``` + +--- + +## Output Format + +```markdown +## Security Review: [File/Component Name] + +### Summary +- **Findings**: X (Y Critical, Z High, ...) +- **Risk Level**: Critical/High/Medium/Low +- **Confidence**: High/Mixed + +### Findings + +#### [VULN-001] [Vulnerability Type] (Severity) +- **Location**: `file.py:123` +- **Confidence**: High +- **Issue**: [What the vulnerability is] +- **Impact**: [What an attacker could do] +- **Evidence**: + ```python + [Vulnerable code snippet] + ``` +- **Fix**: [How to remediate] + +### Needs Verification + +#### [VERIFY-001] [Potential Issue] +- **Location**: `file.py:456` +- **Question**: [What needs to be verified] +``` + +If no vulnerabilities found, state: "No high-confidence vulnerabilities identified." + +--- + +## Reference Files + +### Core Vulnerabilities (`references/`) +| File | Covers | +|------|--------| +| `injection.md` | SQL, NoSQL, OS command, LDAP, template injection | +| `xss.md` | Reflected, stored, DOM-based XSS | +| `authorization.md` | Authorization, IDOR, privilege escalation | +| `authentication.md` | Sessions, credentials, password storage | +| `cryptography.md` | Algorithms, key management, randomness | +| `deserialization.md` | Pickle, YAML, Java, PHP deserialization | +| `file-security.md` | Path traversal, uploads, XXE | +| `ssrf.md` | Server-side request forgery | +| `csrf.md` | Cross-site request forgery | +| `data-protection.md` | Secrets exposure, PII, logging | +| `api-security.md` | REST, GraphQL, mass assignment | +| `business-logic.md` | Race conditions, workflow bypass | +| `modern-threats.md` | Prototype pollution, LLM injection, WebSocket | +| `misconfiguration.md` | Headers, CORS, debug mode, defaults | +| `error-handling.md` | Fail-open, information disclosure | +| `supply-chain.md` | Dependencies, build security | +| `logging.md` | Audit failures, log injection | + +### Language Guides (`languages/`) +- `python.md` - Django, Flask, FastAPI patterns +- `javascript.md` - Node, Express, React, Vue, Next.js +- `go.md` - Go-specific security patterns +- `rust.md` - Rust unsafe blocks, FFI security +- `java.md` - Spring, Java EE patterns + +### Infrastructure (`infrastructure/`) +- `docker.md` - Container security +- `kubernetes.md` - K8s RBAC, secrets, policies +- `terraform.md` - IaC security +- `ci-cd.md` - Pipeline security +- `cloud.md` - AWS/GCP/Azure security diff --git a/.agents/skills/security-review/infrastructure/docker.md b/.agents/skills/security-review/infrastructure/docker.md new file mode 100644 index 0000000000..b66d79aec6 --- /dev/null +++ b/.agents/skills/security-review/infrastructure/docker.md @@ -0,0 +1,432 @@ +# Docker Security Reference + +## Overview + +Container security involves the Dockerfile, image composition, runtime configuration, and orchestration. Misconfigurations can lead to container escapes, privilege escalation, or exposure of sensitive data. + +--- + +## Dockerfile Security + +### Running as Root + +```dockerfile +# VULNERABLE: Running as root (default) +FROM node:18 +COPY . /app +CMD ["node", "app.js"] # Runs as root + +# SAFE: Non-root user +FROM node:18 +RUN groupadd -r appgroup && useradd -r -g appgroup appuser +WORKDIR /app +COPY --chown=appuser:appgroup . . +USER appuser +CMD ["node", "app.js"] + +# SAFE: Using numeric UID (more portable) +USER 1000:1000 +``` + +### Base Image Issues + +```dockerfile +# VULNERABLE: Using latest tag (unpredictable) +FROM node:latest +FROM ubuntu:latest + +# VULNERABLE: Using untrusted/unverified base image +FROM randomuser/myimage + +# SAFE: Pinned versions with digest +FROM node:18.19.0-alpine@sha256:abc123... +FROM python:3.11.7-slim-bookworm + +# SAFE: Official images from verified publishers +FROM docker.io/library/node:18.19.0-alpine +``` + +### Sensitive Data in Images + +```dockerfile +# VULNERABLE: Secrets in build args visible in history +ARG DB_PASSWORD +RUN echo $DB_PASSWORD > /config + +# VULNERABLE: Copying secrets into image +COPY .env /app/.env +COPY secrets.json /app/ +COPY id_rsa /root/.ssh/ + +# VULNERABLE: Secrets in environment variables +ENV API_KEY=sk-12345 +ENV DB_PASSWORD=mysecret + +# SAFE: Mount secrets at runtime +# docker run -v /secrets:/secrets:ro myimage +# Or use Docker secrets in Swarm/K8s +``` + +### Build-Time Secrets + +```dockerfile +# SAFE: Multi-stage build to exclude secrets +FROM node:18 AS builder +# Use build-time secret (Docker BuildKit) +RUN --mount=type=secret,id=npm_token \ + NPM_TOKEN=$(cat /run/secrets/npm_token) npm install + +FROM node:18-alpine +COPY --from=builder /app/node_modules /app/node_modules +# Secret not in final image + +# Build with: docker build --secret id=npm_token,src=.npmrc . +``` + +### Package Installation + +```dockerfile +# VULNERABLE: Not cleaning up package manager cache +RUN apt-get update && apt-get install -y curl wget +# Leaves cache, increases image size and attack surface + +# VULNERABLE: Installing unnecessary packages +RUN apt-get install -y vim nano curl wget git ssh + +# SAFE: Minimal installation with cleanup +RUN apt-get update && \ + apt-get install -y --no-install-recommends curl && \ + rm -rf /var/lib/apt/lists/* + +# SAFE: Using minimal base images +FROM alpine:3.19 +FROM gcr.io/distroless/nodejs18 +FROM scratch # Empty base image +``` + +### COPY vs ADD + +```dockerfile +# VULNERABLE: ADD can auto-extract and fetch URLs +ADD https://example.com/file.tar.gz /app/ # Downloads from URL +ADD archive.tar.gz /app/ # Auto-extracts + +# SAFE: COPY is more explicit +COPY archive.tar.gz /app/ +RUN tar -xzf /app/archive.tar.gz && rm /app/archive.tar.gz +``` + +### Exposed Ports + +```dockerfile +# CHECK: Are all exposed ports necessary? +EXPOSE 22 # FLAG: SSH in container usually unnecessary +EXPOSE 3306 # FLAG: Database port exposed +EXPOSE 80 443 8080 9090 5000 # CHECK: Multiple ports + +# SAFE: Only expose what's needed +EXPOSE 8080 +``` + +--- + +## Image Scanning + +### Vulnerability Patterns + +```bash +# Scan for vulnerabilities +docker scan myimage +trivy image myimage +grype myimage + +# Check for secrets in image +trufflehog docker --image myimage +# Or manually inspect layers +docker history --no-trunc myimage +``` + +### High-Risk Packages + +```dockerfile +# FLAG: Packages that increase attack surface +RUN apt-get install -y \ + openssh-server \ # SSH access + sudo \ # Privilege escalation + netcat \ # Network tools + nmap \ # Network scanning + gcc make \ # Compilers (should be in build stage only) + python3-pip # Package managers (install deps, then remove) +``` + +--- + +## Runtime Security + +### Privileged Mode + +```bash +# VULNERABLE: Running privileged (full host access) +docker run --privileged myimage + +# VULNERABLE: Dangerous capabilities +docker run --cap-add=ALL myimage +docker run --cap-add=SYS_ADMIN myimage +docker run --cap-add=NET_ADMIN myimage + +# SAFE: Drop all capabilities, add only needed +docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myimage + +# SAFE: Read-only root filesystem +docker run --read-only myimage + +# SAFE: No new privileges +docker run --security-opt=no-new-privileges myimage +``` + +### Volume Mounts + +```bash +# VULNERABLE: Mounting sensitive host paths +docker run -v /:/host myimage # Entire host filesystem +docker run -v /etc:/etc myimage # Host config files +docker run -v /var/run/docker.sock:/var/run/docker.sock # Docker socket + +# VULNERABLE: Writable mounts of sensitive paths +docker run -v /etc/passwd:/etc/passwd myimage + +# SAFE: Specific paths, read-only where possible +docker run -v /app/data:/data:ro myimage +docker run -v myvolume:/app/data myimage # Named volume +``` + +### Docker Socket Access + +```bash +# CRITICAL: Docker socket mount = root on host +docker run -v /var/run/docker.sock:/var/run/docker.sock myimage +# Container can create privileged containers, access host + +# If required, use read-only and restrict with authz plugin +# Or use Docker API proxy with limited permissions +``` + +### Network Security + +```bash +# VULNERABLE: Host network mode +docker run --network=host myimage # No network isolation + +# SAFE: User-defined networks with isolation +docker network create --internal internal-net # No external access +docker run --network=internal-net myimage + +# SAFE: Restrict inter-container communication +docker network create --driver=bridge --opt com.docker.network.bridge.enable_icc=false isolated +``` + +### Resource Limits + +```bash +# VULNERABLE: No resource limits (DoS risk) +docker run myimage + +# SAFE: Set memory and CPU limits +docker run --memory=512m --cpus=1 myimage + +# SAFE: Limit processes +docker run --pids-limit=100 myimage +``` + +--- + +## Docker Compose Security + +### Secrets Management + +```yaml +# VULNERABLE: Secrets in environment +services: + app: + environment: + - DB_PASSWORD=mysecret + - API_KEY=sk-12345 + +# SAFE: Use secrets +services: + app: + secrets: + - db_password + environment: + - DB_PASSWORD_FILE=/run/secrets/db_password + +secrets: + db_password: + external: true # Or file: ./secrets/db_password +``` + +### Privilege Restrictions + +```yaml +# SAFE: Security options in compose +services: + app: + image: myimage + user: "1000:1000" + read_only: true + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + cap_add: + - NET_BIND_SERVICE + tmpfs: + - /tmp + deploy: + resources: + limits: + memory: 512M + cpus: '1' +``` + +### Network Isolation + +```yaml +# SAFE: Internal networks for backend services +services: + frontend: + networks: + - public + - internal + + backend: + networks: + - internal # Not accessible from outside + + database: + networks: + - internal + +networks: + public: + internal: + internal: true # No external access +``` + +--- + +## .dockerignore + +### Required Exclusions + +```dockerignore +# SAFE: Exclude sensitive files +.env +.env.* +*.pem +*.key +id_rsa* +secrets/ +credentials/ +.git/ +.gitignore +.dockerignore +Dockerfile +docker-compose*.yml +*.log +node_modules/ +__pycache__/ +.pytest_cache/ +coverage/ +.nyc_output/ +``` + +### Missing .dockerignore + +```bash +# FLAG: No .dockerignore may copy secrets into image +# Check if .env, keys, or credentials are copied +``` + +--- + +## Registry Security + +### Image Pull Policy + +```yaml +# VULNERABLE: Always pulling latest +image: myregistry/myimage:latest + +# VULNERABLE: No digest verification +image: myregistry/myimage:1.0 + +# SAFE: Pinned with digest +image: myregistry/myimage@sha256:abc123... +``` + +### Private Registry Auth + +```bash +# VULNERABLE: Credentials in plain text +docker login -u user -p password registry.example.com + +# SAFE: Use credential helpers +# ~/.docker/config.json +{ + "credHelpers": { + "gcr.io": "gcloud", + "*.dkr.ecr.*.amazonaws.com": "ecr-login" + } +} +``` + +--- + +## Grep Patterns for Dockerfiles + +```bash +# Running as root +grep -rn "^USER" Dockerfile || echo "No USER directive - runs as root" + +# Secrets in environment +grep -rn "^ENV.*PASSWORD\|^ENV.*SECRET\|^ENV.*KEY\|^ENV.*TOKEN" Dockerfile + +# Secrets in build args +grep -rn "^ARG.*PASSWORD\|^ARG.*SECRET\|^ARG.*KEY" Dockerfile + +# Latest tags +grep -rn "FROM.*:latest\|FROM.*@" Dockerfile | grep -v "@sha256" + +# Privileged instructions +grep -rn "^ADD\|EXPOSE 22\|apt-get install.*ssh" Dockerfile + +# Missing cleanup +grep -rn "apt-get install" Dockerfile | grep -v "rm -rf" +``` + +--- + +## Testing Checklist + +- [ ] Container runs as non-root user +- [ ] Base image is pinned with digest +- [ ] No secrets in image layers (ENV, ARG, COPY) +- [ ] Multi-stage build for secrets/build tools +- [ ] Minimal base image (alpine, distroless) +- [ ] Package manager cache cleaned +- [ ] .dockerignore excludes sensitive files +- [ ] No --privileged or dangerous capabilities +- [ ] No Docker socket mount +- [ ] Resource limits configured +- [ ] Network isolation configured +- [ ] Image scanned for vulnerabilities +- [ ] Read-only root filesystem where possible + +--- + +## References + +- [Docker Security Best Practices](https://docs.docker.com/develop/security-best-practices/) +- [CIS Docker Benchmark](https://www.cisecurity.org/benchmark/docker) +- [OWASP Docker Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html) diff --git a/.agents/skills/security-review/languages/javascript.md b/.agents/skills/security-review/languages/javascript.md new file mode 100644 index 0000000000..3a2f241f11 --- /dev/null +++ b/.agents/skills/security-review/languages/javascript.md @@ -0,0 +1,388 @@ +# JavaScript/TypeScript Security Patterns + +## Framework Detection + +| Indicator | Framework | +|-----------|-----------| +| `import React`, `jsx`, `tsx`, `useState` | React | +| `import Vue`, `.vue` files, `v-bind`, `v-model` | Vue | +| `import express`, `app.get`, `app.post` | Express | +| `import { Controller }`, `@nestjs` | NestJS | +| `import next`, `getServerSideProps` | Next.js | +| `import angular`, `@Component` | Angular | + +--- + +## React + +### Auto-Escaped (Do Not Flag) + +```jsx +// SAFE: JSX auto-escapes interpolated values +
{userInput}
+{user.name} +

{data.description}

+ +// SAFE: Setting attributes (except href/src) +
+ +
+``` + +### Flag These (React-Specific) + +```jsx +// XSS - Explicit unsafe rendering +
// FLAG: Critical +// Only safe if userInput is sanitized with DOMPurify or similar + +// URL-based XSS +Link // FLAG: Check for javascript: protocol +