---
name: owasp-security-testing
description: 'Review, audit, and fix code for OWASP Top 10 security vulnerabilities in PHP/Drupal projects. Use when: performing security review, auditing code for vulnerabilities, checking for SQL injection XSS CSRF broken auth insecure dependencies, hardening Drupal modules or themes, reviewing access control, fixing security issues, pentesting checklist, OWASP compliance.'
---

# OWASP Security Testing for PHP/Drupal

## When to Use
- Security review of new or existing code
- Pre-deployment security audit
- Investigating potential vulnerabilities
- Reviewing access control and authentication logic
- Checking third-party/contrib module usage
- Hardening custom modules or themes

---

## OWASP Top 10 — Drupal/PHP Checklist

### A01: Broken Access Control

**What to check:**
- Every route has `_permission`, `_role`, or `_access` requirement in routing YAML
- Custom access callbacks return `AccessResult` objects (not raw booleans)
- Entity operations check `->access('view'|'update'|'delete', $account)`  
- No user-supplied IDs are used to load entities without access checking
- `accessCheck(TRUE)` is set on all entity queries

```php
// BAD — no access check
$query->accessCheck(FALSE)->execute();

// GOOD
$node = $this->entityTypeManager->getStorage('node')->load($nid);
if (!$node->access('view', $this->currentUser)) {
  throw new AccessDeniedHttpException();
}
```

**Routing example:**
```yaml
mymodule.admin_page:
  path: '/admin/my-page'
  requirements:
    _permission: 'administer site configuration'
```

---

### A02: Cryptographic Failures

**What to check:**
- Passwords use `\Drupal\Core\Password\PasswordInterface` (never `md5`, `sha1`)
- Sensitive data is not stored in plain text in config or the database
- HTTPS enforced on Pantheon (automatic) — check `pantheon.yml` for `enforce_https`
- API keys/secrets use Pantheon Secrets (`drupal/pantheon_secrets`) not hardcoded

```php
// GOOD — hash password
$password_service = \Drupal::service('password');
$hashed = $password_service->hash($plain_text_password);

// BAD
$hashed = md5($password);
```

---

### A03: Injection

**SQL Injection — What to check:**
- No raw string concatenation in database queries
- Placeholders used for all user input
- Entity Query API used instead of raw SQL wherever possible

```php
// BAD
$db->query("SELECT * FROM {node} WHERE title = '" . $input . "'");

// GOOD — entity query
$ids = $storage->getQuery()
  ->condition('title', $input)
  ->accessCheck(TRUE)
  ->execute();

// GOOD — db API with placeholder
$db->select('node_field_data', 'n')
  ->condition('n.title', '%' . $db->escapeLike($input) . '%', 'LIKE')
  ->execute();
```

**Twig/HTML injection — What to check:**
- Variables auto-escaped in Twig (`{{ var }}` not `{{ var|raw }}`)
- `|raw` filter only used on content already sanitised by `Xss::filterAdmin()` or `check_markup()`
- `Xss::filter()` applied to user-submitted HTML
- `Html::escape()` for plain text in PHP render arrays

```twig
{# BAD #}
{{ user_content|raw }}

{# GOOD — auto-escaped #}
{{ user_content }}
```

---

### A04: Insecure Design

**What to check:**
- Rate limiting on forms (CAPTCHA via `drupal/recaptcha` or `drupal/recaptcha_v3`)
- No sensitive operations on GET requests (use POST + CSRF token)
- CSRF token validation on custom form submissions and AJAX callbacks
- Mass-assignment protection — never pass raw `$_POST` directly to entity saves

```php
// CSRF token in custom route
use Drupal\Core\Access\CsrfTokenGenerator;

$token = $this->csrfToken->get($path);
// Validate:
if (!$this->csrfToken->validate($token, $path)) {
  throw new AccessDeniedHttpException();
}
```

---

### A05: Security Misconfiguration

**What to check:**
- Error pages don't expose stack traces on production (`error_level: 0` in `services.yml`)
- `services.yml` debug mode disabled in production
- `settings.php` does not expose database credentials in non-Pantheon environments
- `$settings['hash_salt']` is set and unique
- `trusted_host_patterns` is configured in `settings.php`
- No unnecessary modules enabled (review via `drush pml --status=enabled`)
- `web/sites/default/files` not browsable (check `.htaccess`)

```php
// settings.php — required
$settings['trusted_host_patterns'] = ['^example\.com$', '^www\.example\.com$'];
```

---

### A06: Vulnerable and Outdated Components

**What to check:**
- Run `composer audit` to check for known CVEs in dependencies
- Drupal core and contrib modules are up to date (`drush pm:security`)
- Patched packages documented in `composer.patches.json` with source URLs
- No abandoned Composer packages in `require` or `require-dev`

```bash
# Check for security advisories
composer audit
vendor/bin/drush pm:security

# List outdated packages
composer outdated --direct
```

---

### A07: Identification and Authentication Failures

**What to check:**
- Brute-force protection enabled (modules: `drupal/advban`, `drupal/autoban`)
- Password policy enforced (complexity, minimum length)
- Sessions invalidated on logout (`\Drupal::service('session_manager')->destroy()`)
- Account enumeration prevented (generic error messages on login failure)
- Admin account (uid=1) uses a strong, unique password — not `admin`

---

### A08: Software and Data Integrity Failures

**What to check:**
- Deserialization of user-supplied data avoided (never `unserialize($_POST[...])`)
- File uploads validated with `file_validate_extensions()` and MIME type checks
- Pantheon's `php-tuf` integration active for package integrity
- Composer packages use `--prefer-dist` (hashed archives) not `--prefer-source`

```php
// File upload validation
$validators = [
  'file_validate_extensions' => ['jpg jpeg png gif pdf'],
  'file_validate_size' => [25 * 1024 * 1024],
];
$file = file_save_upload('upload', $validators, 'public://uploads/', FILE_EXISTS_RENAME);
```

---

### A09: Security Logging and Monitoring Failures

**What to check:**
- Security-relevant events logged via `\Drupal\Core\Logger\LoggerChannelInterface`
- `drupal/events_log_track` module enabled and configured
- Watchdog entries reviewed for anomalies
- No sensitive data (passwords, tokens, PII) written to logs

```php
// Inject logger
use Psr\Log\LoggerInterface;

$this->logger->warning('Access denied for user @uid on route @route', [
  '@uid' => $this->currentUser->id(),
  '@route' => $route_name,
]);
```

---

### A10: Server-Side Request Forgery (SSRF)

**What to check:**
- URLs passed to `http_client` (Guzzle) are validated before use
- No user-supplied URLs fetched without an allowlist
- Redirect destinations validated with `UrlHelper::isValid()` and same-host check

```php
use Drupal\Component\Utility\UrlHelper;

// Validate redirect destination
$destination = $this->requestStack->getCurrentRequest()->query->get('destination');
if ($destination && !UrlHelper::isExternal($destination)) {
  // Safe internal redirect
}
// NEVER redirect to external user-supplied URLs without explicit allowlist
```

---

## Security Review Procedure

1. **Scan for common patterns** using grep:
   ```bash
   # Find raw queries
   grep -rn "->query(" web/modules/custom --include="*.php"
   # Find |raw in Twig
   grep -rn "|raw" web/themes/custom web/modules/custom --include="*.html.twig"
   # Find unserialize
   grep -rn "unserialize(" web/modules/custom --include="*.php"
   # Find accessCheck(FALSE)
   grep -rn "accessCheck(FALSE)" web/modules/custom --include="*.php"
   ```

2. **Run Composer audit:** `composer audit`

3. **Check Drupal security advisories:** `vendor/bin/drush pm:security`

4. **Review routing YAML** for missing `_permission` requirements

5. **Review custom form handlers** for CSRF and input validation

6. **Check file upload handlers** for extension and MIME validation

7. **Verify access control** on all AJAX callbacks and REST endpoints

8. **Review logs** for unexpected errors or access denials (`admin/reports/dblog`)

---

## References
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
- [Drupal Security Team](https://www.drupal.org/drupal-security-team)
- [Drupal Security Best Practices](https://www.drupal.org/docs/security-in-drupal)
- [Drupal SA Advisories](https://www.drupal.org/security)
- [.github/copilot/security-performance.md](../../copilot/security-performance.md)
