Skip to content
Back to skills

948 Security Guide 4c891e97

ASecurity

Comprehensive guide to Apex security including CRUD/FLS enforcement, sharing rules, SOQL injection prevention, and generation guardrails. ---

  • 9 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 11, 2026
securitygorailstestingcode-reviewapidatabasesecuritydocumentation

Works with

  • api

Security analysis

A100/100

Scanned October 11, 2026

npx -y skills add tools-only/X-Skills --skill 948-security-guide_4c891e97 --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of 948 Security Guide 4c891e97?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for 948 Security Guide 4c891e97
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tools-only-948-security-guide-4c891e97/badge)](https://www.skillsdirectory.com/skills/tools-only-948-security-guide-4c891e97)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

SKILL.md
# Apex Security Guide

Comprehensive guide to Apex security including CRUD/FLS enforcement, sharing rules, SOQL injection prevention, and generation guardrails.

---

## Table of Contents

1. [Generation Guardrails](#generation-guardrails)
2. [CRUD and FLS (Field-Level Security)](#crud-and-fls-field-level-security)
3. [Sharing and Record Access](#sharing-and-record-access)
4. [SOQL Injection Prevention](#soql-injection-prevention)
5. [Security Checklist](#security-checklist)

---

## Generation Guardrails

### ⛔ MANDATORY PRE-GENERATION CHECKS

**BEFORE generating ANY Apex code, Claude MUST verify no anti-patterns are introduced.**

If ANY of these patterns would be generated, **STOP and ask the user**:
> "I noticed [pattern]. This will cause [problem]. Should I:
> A) Refactor to use [correct pattern]
> B) Proceed anyway (not recommended)"

| Anti-Pattern | Detection | Impact | Correct Pattern |
|--------------|-----------|--------|-----------------|
| SOQL inside loop | `for(...) { [SELECT...] }` | Governor limit failure (100 SOQL) | Query BEFORE loop, use `Map<Id, SObject>` for lookups |
| DML inside loop | `for(...) { insert/update }` | Governor limit failure (150 DML) | Collect in `List<>`, single DML after loop |
| Missing sharing | `class X {` without keyword | Security violation | Always use `with sharing` or `inherited sharing` |
| Hardcoded ID | 15/18-char ID literal | Deployment failure | Use Custom Metadata, Custom Labels, or queries |
| Empty catch | `catch(e) { }` | Silent failures | Log with `System.debug()` or rethrow |
| String concatenation in SOQL | `'SELECT...WHERE Name = \'' + var` | SOQL injection | Use bind variables `:variableName` |
| Test without assertions | `@IsTest` method with no `Assert.*` | False positive tests | Use `Assert.areEqual()` with message |

**DO NOT generate anti-patterns even if explicitly requested.** Ask user to confirm the exception with documented justification.

---

### Example: Detecting SOQL in Loop

**BAD (BLOCKED):**
```apex
for (Account acc : accounts) {
    List<Contact> contacts = [SELECT Id FROM Contact WHERE AccountId = :acc.Id];
    // Process contacts
}
```

**GOOD (APPROVED):**
```apex
// Query ONCE before loop
Map<Id, List<Contact>> contactsByAccountId = new Map<Id, List<Contact>>();
for (Contact con : [SELECT Id, AccountId FROM Contact WHERE AccountId IN :accountIds]) {
    if (!contactsByAccountId.containsKey(con.AccountId)) {
        contactsByAccountId.put(con.AccountId, new List<Contact>());
    }
    contactsByAccountId.get(con.AccountId).add(con);
}

// Then loop
for (Account acc : accounts) {
    List<Contact> contacts = contactsByAccountId.get(acc.Id) ?? new List<Contact>();
    // Process contacts
}
```

---

## CRUD and FLS (Field-Level Security)

### API 62.0: WITH USER_MODE

**Modern approach (API 62.0+)**: Use `WITH USER_MODE` in SOQL to enforce CRUD and FLS automatically.

```apex
// ✅ GOOD: Respects user permissions
List<Account> accounts = [
    SELECT Id, Name, Industry, AnnualRevenue
    FROM Account
    WHERE Industry = 'Technology'
    WITH USER_MODE
];
```

**What it does**:
- Enforces object-level CRUD (Create, Read, Update, Delete)
- Enforces field-level security (FLS)
- Throws `System.QueryException` if user lacks access
- Respects user's sharing rules (when combined with `with sharing`)

**When to use SYSTEM_MODE**:
```apex
// Only use SYSTEM_MODE when you explicitly NEED to bypass security
List<Account> accounts = [
    SELECT Id, Name, Sensitive_Field__c
    FROM Account
    WITH SYSTEM_MODE  // ⚠️ Use with caution!
];
```

**Use cases for SYSTEM_MODE**:
- Background jobs that must process all records regardless of user
- System integrations
- Administrative cleanup scripts

**ALWAYS document why SYSTEM_MODE is needed:**
```apex
// JUSTIFICATION: This batch job processes all accounts for regulatory reporting,
// regardless of user's access level. Approved by Security Team on 2025-01-01.
```

---

### Legacy Approach: Security.stripInaccessible()

**For pre-62.0 compatibility** or when you need to filter fields dynamically:

```apex
// Query all fields
List<Account> accounts = [SELECT Id, Name, Industry, AnnualRevenue FROM Account];

// Strip inaccessible fields
SObjectAccessDecision decision = Security.stripInaccessible(
    AccessType.READABLE,
    accounts
);

// Use stripped records
List<Account> accessibleAccounts = decision.getRecords();

// Check which fields were removed
Set<String> removedFields = decision.getRemovedFields().get('Account');
if (removedFields != null && !removedFields.isEmpty()) {
    System.debug('User lacks access to fields: ' + removedFields);
}
```

**Access Types**:
- `READABLE` - Read access (for queries)
- `CREATABLE` - Create access (before insert)
- `UPDATABLE` - Update access (before update)
- `UPSERTABLE` - Upsert access

**Example: Pre-DML Check**
```apex
public static void createAccounts(List<Account> accounts) {
    // Check if user can create these fields
    SObjectAccessDecision decision = Security.stripInaccessible(
        AccessType.CREATABLE,
        accounts
    );

    if (!decision.getRemovedFields().isEmpty()) {
        throw new SecurityException('User lacks permission to create some fields');
    }

    insert decision.getRecords();
}
```

---

### Manual CRUD/FLS Checks (Verbose but Explicit)

```apex
// Check object-level CRUD
if (!Schema.sObjectType.Account.isAccessible()) {
    throw new SecurityException('User cannot read Accounts');
}

if (!Schema.sObjectType.Account.isCreateable()) {
    throw new SecurityException('User cannot create Accounts');
}

// Check field-level security
if (!Schema.sObjectType.Account.fields.Industry.isAccessible()) {
    throw new SecurityException('User cannot read Industry field');
}

if (!Schema.sObjectType.Account.fields.Industry.isUpdateable()) {
    throw new SecurityException('User cannot update Industry field');
}
```

**When to use**: Legacy codebases, specific error messaging, or when you need fine-grained control.

---

## Sharing and Record Access

### Sharing Keywords

| Keyword | Behavior | When to Use |
|---------|----------|-------------|
| `with sharing` | Enforces record-level sharing | Default for user-facing code |
| `without sharing` | Bypasses record-level sharing | System operations, integrations |
| `inherited sharing` | Inherits from calling class | Utility classes, shared libraries |

**Default Rule**: If no keyword specified, class runs in `without sharing` mode (pre-API 40 behavior).

**ALWAYS specify a sharing keyword** - implicit behavior is confusing.

---

### with sharing (Recommended Default)

```apex
public with sharing class AccountService {

    public static List<Account> getAccountsForUser() {
        // User only sees Accounts they have access to via sharing rules
        return [SELECT Id, Name FROM Account WITH USER_MODE];
    }

    public static void updateAccount(Account acc) {
        // Throws exception if user lacks access
        update acc;
    }
}
```

**Use cases**:
- User-facing controllers (LWC, Aura, Visualforce)
- Service classes handling user requests
- Trigger actions that respect user context

---

### without sharing (Use Sparingly)

```apex
public without sharing class AdminService {

    // JUSTIFICATION: This method is only called by system administrators
    // to perform global updates. Access controlled by Custom Permission.
    public static void globalAccountUpdate() {
        List<Account> allAccounts = [SELECT Id, Name FROM Account];
        // Process ALL accounts, ignoring sharing
    }
}
```

**Use cases**:
- Background jobs
- System integrations
- Administrative operations

**Security Note**: Always add access control checks when using `without sharing`:
```apex
public without sharing class AdminService {

    public static void globalUpdate() {
        // Check permission before executing
        if (!FeatureManagement.checkPermission('Admin_Global_Update')) {
            throw new SecurityException('Requires Admin_Global_Update permission');
        }

        // Now safe to proceed with without sharing logic
    }
}
```

---

### inherited sharing (Best for Utilities)

```apex
public inherited sharing class StringUtils {

    // Inherits sharing from calling class
    public static String sanitize(String input) {
        return String.escapeSingleQuotes(input);
    }
}

// Called from "with sharing" class → runs with sharing
// Called from "without sharing" class → runs without sharing
```

**Use cases**:
- Utility classes
- Helper methods
- Shared libraries that don't directly query records

---

### Mixing Sharing Contexts

```apex
public with sharing class UserFacingService {

    public static void processAccount(Id accountId) {
        // This runs WITH sharing
        Account acc = [SELECT Id, Name FROM Account WHERE Id = :accountId];

        // Call a without sharing method for specific operation
        SystemOperations.performGlobalCheck(acc);
    }
}

public without sharing class SystemOperations {

    public static void performGlobalCheck(Account acc) {
        // This runs WITHOUT sharing
        // Can access records the original user couldn't see
    }
}
```

**Pattern**: Start with `with sharing`, only escalate to `without sharing` when needed.

---

## SOQL Injection Prevention

### The Problem

**NEVER concatenate user input into SOQL strings:**

```apex
// ❌ VULNERABLE to SOQL injection
public static List<Account> searchAccounts(String userInput) {
    String query = 'SELECT Id, Name FROM Account WHERE Name = \'' + userInput + '\'';
    return Database.query(query);
}

// Attack: userInput = "test' OR '1'='1"
// Results in: SELECT Id, Name FROM Account WHERE Name = 'test' OR '1'='1'
// Returns ALL accounts!
```

---

### Solution 1: Bind Variables (Recommended)

```apex
// ✅ SAFE: Use bind variables
public static List<Account> searchAccounts(String userInput) {
    return [SELECT Id, Name FROM Account WHERE Name = :userInput WITH USER_MODE];
}

// Even with malicious input, it's treated as a literal string
```

**Why it works**: Salesforce treats `:userInput` as a value, not executable SOQL.

---

### Solution 2: String.escapeSingleQuotes()

**When dynamic SOQL is unavoidable** (rare cases):

```apex
// ✅ SAFE: Escape user input
public static List<Account> dynamicSearch(String userInput) {
    String sanitized = String.escapeSingleQuotes(userInput);
    String query = 'SELECT Id, Name FROM Account WHERE Name = \'' + sanitized + '\'';
    return Database.query(query);
}
```

**What it does**: Escapes single quotes (`'` → `\'`) to prevent breaking out of string literals.

**Still prefer bind variables** - escapeSingleQuotes is a backup.

---

### Solution 3: Allowlist Validation

**For field names, operators, or other dynamic query parts:**

```apex
public static List<Account> sortedAccounts(String sortField) {
    // ✅ SAFE: Validate against allowlist
    Set<String> allowedFields = new Set<String>{'Name', 'Industry', 'AnnualRevenue'};

    if (!allowedFields.contains(sortField)) {
        throw new IllegalArgumentException('Invalid sort field');
    }

    String query = 'SELECT Id, Name, Industry FROM Account ORDER BY ' + sortField;
    return Database.query(query);
}
```

**Use case**: Dynamic ORDER BY, dynamic field selection (but NOT WHERE clause values).

---

### Dynamic SOQL Best Practices

**Pattern: Safe Dynamic Query Builder**
```apex
public class SafeQueryBuilder {

    private static final Set<String> ALLOWED_FIELDS = new Set<String>{
        'Id', 'Name', 'Industry', 'AnnualRevenue'
    };

    private static final Set<String> ALLOWED_OPERATORS = new Set<String>{
        '=', '!=', '<', '>', '<=', '>=', 'LIKE', 'IN'
    };

    public static List<Account> query(
        String field,
        String operator,
        String value
    ) {
        // Validate field
        if (!ALLOWED_FIELDS.contains(field)) {
            throw new IllegalArgumentException('Invalid field: ' + field);
        }

        // Validate operator
        if (!ALLOWED_OPERATORS.contains(operator)) {
            throw new IllegalArgumentException('Invalid operator: ' + operator);
        }

        // Use bind variable for value
        String query = 'SELECT Id, Name FROM Account WHERE ' + field + ' ' + operator + ' :value WITH USER_MODE';
        return Database.query(query);
    }
}
```

---

## Security Checklist

Use this checklist when generating or reviewing Apex code:

### CRUD/FLS

- [ ] All SOQL queries use `WITH USER_MODE` (or `Security.stripInaccessible()` for pre-62.0)
- [ ] DML operations check `isCreateable()`, `isUpdateable()`, `isDeletable()` OR use `WITH USER_MODE`
- [ ] Custom fields have Permission Sets/Profiles granting FLS
- [ ] System operations using `WITH SYSTEM_MODE` are documented with justification

### Sharing

- [ ] All classes have explicit sharing keyword (`with sharing`, `without sharing`, or `inherited sharing`)
- [ ] User-facing classes use `with sharing`
- [ ] `without sharing` classes have documented justification
- [ ] `without sharing` classes include Custom Permission checks

### SOQL Injection

- [ ] No string concatenation in WHERE clauses with user input
- [ ] All user input uses bind variables (`:variableName`)
- [ ] Dynamic SOQL uses allowlist validation for field names/operators
- [ ] `String.escapeSingleQuotes()` used if concatenation is unavoidable

### General Security

- [ ] No hardcoded credentials or API keys (use Named Credentials)
- [ ] No hardcoded Record IDs (use Custom Metadata or queries)
- [ ] Sensitive data (SSN, PII) is encrypted at rest (Platform Encryption)
- [ ] External callouts use Named Credentials, not plain URLs
- [ ] Error messages don't leak sensitive information

---

## Advanced Security Patterns

### Custom Permissions

**Check user has specific permission before dangerous operations:**

```apex
public without sharing class DataDeletionService {

    public static void deleteAllTestData() {
        // Check Custom Permission
        if (!FeatureManagement.checkPermission('Delete_Test_Data')) {
            throw new SecurityException('Requires Delete_Test_Data permission');
        }

        // Safe to proceed
        delete [SELECT Id FROM Account WHERE Name LIKE 'TEST%'];
    }
}
```

**Create Custom Permission**: Setup → Custom Permissions → New

---

### Secure Remote Actions (@AuraEnabled)

```apex
public with sharing class AccountController {

    @AuraEnabled(cacheable=true)
    public static List<Account> getAccounts() {
        // Runs with sharing + user mode = secure
        return [SELECT Id, Name FROM Account WITH USER_MODE LIMIT 50];
    }

    @AuraEnabled
    public static void updateAccount(Account acc) {
        // Verify user can update
        if (!Schema.sObjectType.Account.isUpdateable()) {
            throw new AuraHandledException('No update permission');
        }

        update acc;
    }
}
```

**Security Notes**:
- Always use `with sharing` for `@AuraEnabled` methods
- Use `WITH USER_MODE` in SOQL
- Validate DML permissions before operations
- Use `AuraHandledException` for friendly error messages

---

### Platform Events Security

```apex
public with sharing class EventPublisher {

    public static void publishEvent(String message) {
        // Check if user can create Platform Events
        if (!Schema.sObjectType.MyEvent__e.isCreateable()) {
            throw new SecurityException('Cannot publish events');
        }

        MyEvent__e event = new MyEvent__e(
            Message__c = message
        );

        EventBus.publish(event);
    }
}
```

---

## Common Security Vulnerabilities

| Vulnerability | Example | Fix |
|---------------|---------|-----|
| **SOQL Injection** | `'WHERE Name = \'' + input + '\''` | Use bind variable `:input` |
| **XSS (Cross-Site Scripting)** | Returning unsanitized HTML | Use `HTMLENCODE()` in VF or LWC escaping |
| **Insecure Direct Object Reference** | Accepting record ID from user without checking access | Query with `WITH USER_MODE`, verify in `with sharing` |
| **Hardcoded Credentials** | `String apiKey = 'abc123';` | Use Named Credentials |
| **Missing FLS** | Directly querying fields without checking | Use `WITH USER_MODE` |
| **Overly Permissive Sharing** | `without sharing` everywhere | Use `with sharing` by default |

---

## Testing Security

### Test User Mode

```apex
@IsTest
static void testUserModeEnforcesPermissions() {
    // Create user without Account read access
    User restrictedUser = createRestrictedUser();

    System.runAs(restrictedUser) {
        try {
            List<Account> accounts = [SELECT Id FROM Account WITH USER_MODE];
            Assert.fail('Expected QueryException for user without access');
        } catch (System.QueryException e) {
            Assert.isTrue(e.getMessage().contains('Insufficient privileges'));
        }
    }
}
```

### Test Sharing Rules

```apex
@IsTest
static void testSharingEnforcement() {
    Account acc = new Account(Name = 'Test Account', OwnerId = UserInfo.getUserId());
    insert acc;

    // Create user who is NOT the owner
    User otherUser = createStandardUser();

    System.runAs(otherUser) {
        // Should NOT see account owned by different user
        List<Account> visible = [SELECT Id FROM Account WHERE Id = :acc.Id];
        Assert.areEqual(0, visible.size(), 'User should not see account due to sharing rules');
    }
}
```

---

## Reference

**Full Documentation**: See `docs/` folder for comprehensive guides:
- `security-guide.md` - Complete security reference (this is an extract)
- `best-practices.md` - Includes security best practices
- `code-review-checklist.md` - Security scoring criteria

**Back to Main**: [SKILL.md](../SKILL.md)

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.