---
title: "How I Made Claude Code Enforce OWASP Rules (So I Don't Have To)"
description: "Claude Code skills can automatically enforce security patterns, catch OWASP violations, and flag accessibility issues before they ship. Here's the security skill I built for my team."
date: "2026-03-02"
tags: [ai, claude-code, security, owasp, accessibility, skills]
---
Every developer knows the rules. Validate input. Use parameterized queries. Never log passwords. Check CSRF tokens. Don't bind directly to your domain model.

But here's the thing: knowing the rules and *following* them under pressure are two different activities. You're deep in a feature sprint, your PR is already massive, and you just need this one controller action to work. You'll come back and add the anti-forgery token later. You'll refactor that raw SQL query next week. You'll add the `aria-label` before it ships.

You won't. I know because I've been on both sides of that code review.

So I did something about it. I built a Claude Code skill that enforces OWASP Top 10 rules on every piece of code Claude writes for me. Not when I ask. Not when I remember. Every time. Automatically.

Here's how, and why your team should steal it.

## Table of Contents
1. [The Problem With Security Knowledge](#the-problem)
2. [What Are Skills (30-Second Version)](#what-are-skills)
3. [The Security Skill I Built](#the-security-skill)
4. [OWASP Top 10 in Practice](#owasp-in-practice)
5. [Accessibility Is Security Too](#accessibility)
6. [Sharing This With Your Team](#sharing)
7. [TL;DR](#tldr)

---

## The Problem With Security Knowledge {#the-problem}

Security knowledge lives in three places at most companies:

1. **The docs nobody reads.** Confluence pages written two years ago by someone who left. Maybe an internal wiki with "Security Best Practices" that was last updated before your framework got a major version bump.

2. **The heads of senior devs.** They catch it in code review. Sometimes. When they have time. When the PR isn't too big to read carefully.

3. **The CI pipeline.** Static analysis tools that flag issues after the code is written, often with so many false positives that developers stop reading the output.

None of these prevent insecure code from being written in the first place. They're all reactive. The damage is already done by the time someone catches `FromSqlRaw` with string concatenation.

What if the tool writing the code just... already knew the rules?

That's what Claude Code skills do. And the security skill takes about 15 minutes to set up. It saves hours of code review comments and prevents entire categories of vulnerabilities from ever entering your codebase.

---

## What Are Skills (30-Second Version) {#what-are-skills}

A skill is a `SKILL.md` file with YAML frontmatter. Claude reads it automatically when the task matches the description. That's it.

```yaml
---
name: security
description: Use ALWAYS when writing any code.
---

## Your Security Rules Here
```

The `description` field is the trigger. When it says "Use ALWAYS when writing any code," Claude loads those instructions on every single task. No slash command needed. No manual invocation. It just applies.

I wrote a full deep dive on skills in [Claude Skills: What They Are, Why You Need Them, and How to Set Them Up](/blog/claude-skills-setup-guide-why-you-need-them). This post is specifically about what I put *in* the security skill and why.

---

## The Security Skill I Built {#the-security-skill}

The frontmatter is the most important part:

```yaml
---
name: security
description: Use ALWAYS when writing any code. Enforce OWASP Top 10
  protections, secure coding patterns, input validation, and vulnerability
  prevention in .NET, Blazor, Razor, JavaScript, and all web code.
---
```

Two things matter here:

**"Use ALWAYS when writing any code."** Most skills trigger on specific tasks ("Use when reviewing pull requests" or "Use when writing frontend UI"). This one fires every time. Because security isn't a feature you add later. It's a constraint on everything.

**The specific framework list.** Mentioning ".NET, Blazor, Razor, JavaScript" helps Claude match the skill to the right context. If you're working in a different stack, swap these out. The patterns translate, the framework names are just for matching.

The body of the skill is structured around the OWASP Top 10, with WRONG/RIGHT code examples for each category. Claude doesn't need theory. It needs concrete patterns to follow and anti-patterns to avoid.

---

## OWASP Top 10 in Practice {#owasp-in-practice}

Here are the five rules that catch the most real-world issues. These are directly from the skill, with the exact examples Claude reads.

### 1. SQL Injection

The most basic vulnerability. Still shows up constantly.

```csharp
// WRONG — user input concatenated into SQL
var users = db.Users.FromSqlRaw(
    $"SELECT * FROM Users WHERE Name = '{name}'"
);

// RIGHT — parameterized (auto-parameterized by FromSqlInterpolated)
var users = db.Users.FromSqlInterpolated(
    $"SELECT * FROM Users WHERE Name = {name}"
);

// BEST — use LINQ, no raw SQL needed
var users = db.Users.Where(u => u.Name == name);
```

The skill tells Claude: "NEVER concatenate user input into SQL, commands, or queries. EF Core parameterizes by default. Use it."

With this in the skill, Claude won't generate the first pattern. It defaults to LINQ. If raw SQL is truly needed, it uses `FromSqlInterpolated`. I've never had to send back a PR comment about SQL injection since.

### 2. Cross-Site Scripting (XSS)

Razor auto-encodes output by default. The danger is `Html.Raw()`.

```csharp
// WRONG — renders raw HTML from user input
@Html.Raw(Model.UserComment)

// RIGHT — auto-encoded by Razor
@Model.UserComment

// If you MUST render HTML, sanitize first
@Html.Raw(sanitizer.Sanitize(Model.UserComment))
```

In Blazor, `MarkupString` is the equivalent of `Html.Raw`. Same danger, different syntax. The skill calls both out explicitly so Claude catches it regardless of which rendering model you're using.

### 3. Cross-Site Request Forgery (CSRF)

One missing attribute. One exploitable endpoint.

```csharp
// WRONG — no anti-forgery token
[HttpPost]
public async Task<IActionResult> Delete(int id) { ... }

// RIGHT — always validate the token
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(int id) { ... }
```

The skill says: "ALWAYS use `[ValidateAntiForgeryToken]` on POST/PUT/DELETE actions. NEVER skip this." Short, direct, no room for interpretation.

Every controller action Claude generates for me now includes this attribute. Before the skill, it was about 50/50.

### 4. Mass Assignment / Over-Posting

This is the one that surprises people. It's a .NET-specific vulnerability that's easy to miss.

```csharp
// WRONG — user can POST IsAdmin=true
public async Task<IActionResult> Create(User user) { ... }

// RIGHT — only bind expected fields via DTO
public async Task<IActionResult> Create(CreateUserDto dto) { ... }
```

If you bind directly to your domain model, any property on that model is fair game. A user can add `IsAdmin=true` to the POST body and your framework will happily set it.

The fix is simple: always use DTOs (Data Transfer Objects) that only contain the fields you expect. The skill enforces this pattern by telling Claude to never bind directly to domain models.

### 5. Insecure Direct Object References (IDOR)

The "anyone can edit anything" vulnerability.

```csharp
// WRONG — no ownership check
public async Task<IActionResult> Edit(int id)
    => View(await _service.GetByIdAsync(id));

// RIGHT — verify the user owns/can access this resource
public async Task<IActionResult> Edit(int id)
{
    var user = await _service.GetByIdAsync(id);
    if (user is null) return NotFound();
    if (user.Id != CurrentUserId && !User.IsInRole("Admin"))
        return Forbid();
    return View(user);
}
```

Just because `/users/123/edit` exists doesn't mean the person requesting it should have access. The skill tells Claude to always verify ownership, not just existence.

### What Else the Skill Covers

Beyond these five, the full skill also enforces:

- **Security headers** (X-Content-Type-Options, X-Frame-Options, CSP, Referrer-Policy)
- **Input validation** (Data Annotations, FluentValidation, file upload checking)
- **Secrets management** (never hardcode, use `dotnet user-secrets` locally, Key Vault in production)
- **Logging safety** (never log passwords, tokens, or PII)
- **Unvalidated redirects** (always check `Url.IsLocalUrl` before redirecting)
- **Broken authentication** (ASP.NET Identity, account lockout, strong password policies)
- **Dependency security** (`dotnet list package --vulnerable`)

Each one has the same format: the rule, a WRONG example, a RIGHT example. Claude doesn't need paragraphs of explanation. It needs patterns.

---

## Accessibility Is Security Too {#accessibility}

I built a second skill alongside the security one. The frontend-styling skill includes a full WCAG 2.1 AA section that applies to every view and component.

Here's why I consider it part of the same effort: if your app is unusable for 15-20% of your users, you have a product failure. And if you're in an enterprise, legal, or government context, you have a compliance failure. Accessibility isn't optional and it isn't a nice-to-have.

The accessibility rules follow the same WRONG/RIGHT pattern:

### Forms Without Labels

```html
<!-- WRONG — input has no associated label -->
<input type="email" class="form-control" />

<!-- RIGHT — label with matching for/id -->
<label for="email" class="form-label">
    Email <span class="text-danger" aria-hidden="true">*</span>
</label>
<input id="email" type="email" class="form-control"
       autocomplete="email" aria-required="true"
       aria-describedby="email-error" />
<span id="email-error" class="text-danger small" role="alert"></span>
```

### Icon-Only Buttons

```html
<!-- WRONG — screen reader has no idea what this does -->
<button class="btn btn-sm">
    <i class="bi bi-pencil"></i>
</button>

<!-- RIGHT — aria-label describes the action -->
<button class="btn btn-sm" aria-label="Edit user John Smith">
    <i class="bi bi-pencil" aria-hidden="true"></i>
</button>
```

### Color as the Only Indicator

```html
<!-- WRONG — color is the only way to tell this is an error -->
<span class="text-danger">Error</span>

<!-- RIGHT — icon + color + role for screen readers -->
<span class="text-danger" role="alert">
    <i class="bi bi-exclamation-triangle me-1" aria-hidden="true"></i>
    Error: Email is required
</span>
```

The skill also enforces semantic HTML (`<main>`, `<nav>`, `<header>`, `<footer>`), heading hierarchy (never skip levels), keyboard navigation, `prefers-reduced-motion` support, and minimum touch targets (44x44px).

Same as security: Claude doesn't forget these rules. Once they're in the skill, they apply to every component, every view, every page. No more "we'll add accessibility later."

---

## Sharing This With Your Team {#sharing}

A skill is only as useful as its reach. If you're the only one on the team with the security skill, the rest of the team is still writing insecure code with Claude's help.

Three ways to share:

### Option 1: Per-Project (Committed to the Repo)

```
your-repo/
└── .claude/
    └── skills/
        └── security/
            └── SKILL.md
```

Check it into the repo. Every developer who uses Claude Code on this project gets the skill automatically. This is the best option for project-specific rules.

### Option 2: Global (Personal Machine)

```
~/.claude/skills/
├── security/
│   └── SKILL.md
├── frontend-styling/
│   └── SKILL.md
└── csharp-dotnet/
│   └── SKILL.md
```

Drop them in `~/.claude/skills/` and they apply to every project on your machine. Good for personal standards. On Windows: `%USERPROFILE%\.claude\skills\`.

### Option 3: Shared Git Repo

Create a repo with your skills. Team members clone it into their global skills folder:

```bash
git clone git@github.com:your-org/team-claude-skills.git ~/.claude/skills
```

Updates are a `git pull` away. This is what I did for my team. One repo, one source of truth, everyone on the same page.

**The nuclear option:** Combine approaches. Put project-specific patterns in the repo's `.claude/skills/`. Put universal security rules in a shared team repo cloned to `~/.claude/skills/`. They stack. Claude reads both.

---

## TL;DR {#tldr}

| What | Details |
|------|---------|
| **What it is** | A `SKILL.md` file that makes Claude Code enforce OWASP Top 10 rules automatically |
| **Trigger** | `description: Use ALWAYS when writing any code` makes it fire on every task |
| **What it catches** | SQL injection, XSS, CSRF, mass assignment, IDOR, missing security headers, secrets in code, unsafe logging, unvalidated redirects |
| **Bonus skill** | Frontend-styling skill with WCAG 2.1 AA enforcement (forms, labels, contrast, keyboard nav, screen readers) |
| **Setup time** | 15 minutes to create the SKILL.md, 0 minutes per session after that |
| **Team sharing** | Commit to repo, global install, or shared Git repo |
| **Format** | WRONG/RIGHT code examples. No theory. Patterns Claude can follow. |

The security skill doesn't make Claude smarter about security. Claude already knows OWASP. The skill makes it *consistent*. Every controller gets the anti-forgery token. Every query uses parameterization. Every form gets labels and validation. Not because someone remembered to ask for it, but because the instructions are always loaded.

That's the difference between knowing the rules and following them.

---

**More posts:**
- [Claude Skills: What They Are, Why You Need Them, and How to Set Them Up](/blog/claude-skills-setup-guide-why-you-need-them)
- [My Actual Dev Setup: Google Antigravity + Claude Code](/blog/antigravity-claude-code-workflow-2026)
- [Cursor vs. Windsurf vs. Claude Code: The AI Coding Editor War](/blog/cursor-vs-windsurf-vs-claude-code-ai-coding-editor-war)

---

*// hereshecodes.com*
