# CC Soccer — Security Vulnerability Assessment

**Date:** March 16, 2026
**Prepared for:** Andrew Meade
**Scope:** Full codebase review of the CC Soccer Drupal 11 custom module, JavaScript, templates, configuration, and server settings

---

## Executive Summary

This report presents findings from a comprehensive security review of the CC Soccer Drupal 11 website codebase. The review covered all custom PHP controllers, forms, services, JavaScript files, Twig templates, Drupal configuration, and server settings. **16 distinct security issues** were identified.

| Severity | Count | Risk Level | Action Required |
|----------|-------|------------|-----------------|
| **CRITICAL** | 3 | Immediate | Fix before production deployment |
| **HIGH** | 6 | Urgent | Fix within 1–2 weeks |
| **MEDIUM** | 6 | Important | Address in next development cycle |
| **LOW** | 1 | Advisory | Plan for future improvement |

### Remediation Progress (Updated March 16, 2026)

| # | Finding | Severity | Status |
|---|---------|----------|--------|
| 1 | Authorize.net credentials in git | CRITICAL | 🔶 Do at production deployment |
| 2 | Database dump in repo | CRITICAL | ⚠️ **Delete before next commit** |
| 3 | Empty hash_salt | CRITICAL | 🔶 Do at production deployment |
| 4 | Trusted host patterns | HIGH | 🔶 Do at production deployment (test server now, production later) |
| 5 | Missing CSRF tokens on AJAX | HIGH | ✅ **Completed** — `fetchWithCsrf()` added to 8 JS files |
| 6 | Insufficient access checks | HIGH | ✅ **Completed** — `hasPermission()` checks added to 8 controllers |
| 7 | XSS via innerHTML | HIGH | ✅ **Completed** — Safe DOM methods in 4 JS files (+ bonus: tournament-schedule-builder.js) |
| 8 | Inline JS event handlers | HIGH | ⚠️ Still needed — ReportController.php onchange handler |
| 9 | Devel module present | HIGH | 🔶 Do at production deployment (`drush pm:uninstall devel devel_generate`) |
| 10 | Dev services config active | MEDIUM | 🔶 Do at production deployment (verify settings.local.php not deployed) |
| 11 | Stored XSS in notes fields | MEDIUM | ⚠️ Still needed — TournamentDepositForfeitForm.php, TournamentDepositRefundForm.php |
| 12 | Error messages expose details | MEDIUM | ⚠️ Still needed — Replace `$e->getMessage()` with generic messages |
| 13 | Rate limiting | MEDIUM | ✅ **Completed** — Flood control on 3 player-facing endpoints (userSearch, invite, nudge) |
| 14 | CSP headers | MEDIUM | 🔶 Do at production deployment (start with report-only mode) |
| 15 | Inconsistent HTML escaping | MEDIUM | ⚠️ Still needed — Standardize `Html::escape()` in render arrays |
| 16 | Entity access on clone params | LOW | ⚠️ Still needed — SeasonForm.php, TournamentForm.php |

---

## CRITICAL Findings

### 1. Authorize.net API Credentials Committed to Git — 🔶 Production Deployment

**Type:** Credential Exposure
**File:** `config/install/commerce_payment.commerce_payment_gateway.authorize_net_test.yml`

The Authorize.net test gateway configuration contains plaintext API credentials (`api_login`, `transaction_key`, `client_key`) committed directly in the codebase. Even though these are currently test-mode credentials, this pattern is dangerous — if production credentials are ever added similarly, they would be permanently exposed in version control history.

**Fix:** Move payment gateway credentials to `settings.local.php` or environment variables. Use Drupal config overrides:

```php
// In settings.local.php:
$config['commerce_payment.commerce_payment_gateway.authorize_net']['configuration']['api_login'] = getenv('AUTHNET_LOGIN');
$config['commerce_payment.commerce_payment_gateway.authorize_net']['configuration']['transaction_key'] = getenv('AUTHNET_TRANSACTION_KEY');
$config['commerce_payment.commerce_payment_gateway.authorize_net']['configuration']['client_key'] = getenv('AUTHNET_CLIENT_KEY');
```

---

### 2. Database Dump with User Data in Repository — ⚠️ Delete Before Next Commit

**Type:** Data Exposure
**File:** `ccsoccer-d11-migrated-200users.sql` (23MB, 33,779 lines)

A full database dump containing 200 users' data is present in the repository root. This file likely contains email addresses, hashed passwords, personal information, and potentially payment data. While `.gitignore` excludes `*.sql`, the file is already in the working directory and could be accidentally committed.

**Fix:** Delete the SQL file from the repository and verify it was never committed to git history. If it was, use BFG Repo Cleaner to purge it. Store database dumps in a secure, access-controlled location outside the repository (e.g., DDEV snapshots, which are already gitignored).

---

### 3. Empty hash_salt in settings.php — 🔶 Production Deployment

**Type:** Weak Cryptography
**File:** `web/sites/default/settings.php` (line 291)

The `hash_salt` setting is set to an empty string. This salt is used for generating one-time login links, form tokens, and other security-critical hashes. An empty salt significantly weakens the security of these mechanisms, making it easier for attackers to forge tokens.

**Fix:** This is likely overridden by `settings.ddev.php` in local development, but you must ensure production has a unique, strong salt:

```php
$settings['hash_salt'] = 'a-long-random-string-unique-to-this-site';
```

---

## HIGH Findings

### 4. Trusted Host Patterns Not Configured — 🔶 Production Deployment

**Type:** HTTP Host Header Injection
**File:** `web/sites/default/settings.php` (line 776)

The `trusted_host_patterns` setting is commented out. Without this, the site is vulnerable to HTTP Host header attacks, which can lead to cache poisoning, password reset link manipulation, and server-side request forgery.

**Fix:** Configure in `settings.local.php` for production:

```php
$settings['trusted_host_patterns'] = [
  '^ccsoccer\.com$',
  '^www\.ccsoccer\.com$',
];
```

---

### 5. Missing CSRF Tokens on AJAX POST Requests — ✅ Completed March 16, 2026

**Type:** Cross-Site Request Forgery
**Files:** `notification-confirm.js`, `player-skill.js`, `season-players.js`, `tournament-teams.js`, `roster-builder.js`

Multiple JavaScript files make POST requests via `fetch()` without including the `X-CSRF-Token` header. Drupal requires this token for non-form AJAX requests to prevent cross-site request forgery attacks. An attacker could craft a malicious page that triggers these endpoints while a logged-in admin visits it.

**Fix:** Fetch the CSRF token and include it in all POST requests:

```javascript
// Get token once
const tokenResponse = await fetch('/session/token');
const csrfToken = await tokenResponse.text();

// Include in all POST requests
fetch('/api/endpoint', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': csrfToken
  },
  body: JSON.stringify(data)
});
```

---

### 6. Insufficient Access Checks on AJAX Endpoints — ✅ Completed March 16, 2026

**Type:** Broken Access Control / IDOR
**Files:** `PlayerAdminController.php` (saveSkillLevel, rotatePicture), `PlayerSkillController.php` (saveSkillLevel, saveDiscount)

AJAX endpoints allow modifying any user's skill level, discount, or profile picture by passing an arbitrary `user_id` in the JSON payload. While routing-level permissions exist, there is no controller-level verification that the requesting user is authorized to modify that specific user's data.

**Fix:** Add explicit per-entity access checks:

```php
if (!$this->currentUser()->hasPermission('administer ccsoccer')
    && $this->currentUser()->id() != $user_id) {
  return new JsonResponse(['error' => 'Access denied'], 403);
}
```

---

### 7. XSS via innerHTML and Unsafe HTML Concatenation — ✅ Completed March 16, 2026

**Type:** Cross-Site Scripting (XSS)
**Files:** `group-management.js`, `schedule-builder.js`, `tournament-roster-builder.js`, `roster-builder.js`

User-controlled data (player names, team names, emails) is inserted into the DOM via `innerHTML` without consistent escaping. While some files have an `escapeHtml()` function, it is not used uniformly. Malicious usernames or team names could inject JavaScript.

**Fix:** Replace `innerHTML` with `textContent` for text-only content. When HTML structure is needed, use `escapeHtml()` consistently on ALL user-controlled values, or better yet, use DOM creation methods (`createElement` / `appendChild`).

---

### 8. Inline JavaScript Event Handlers with URL Injection — ⚠️ Still Needed

**Type:** XSS / URL Injection
**File:** `ReportController.php` (line 387)

The tournament deposit report uses `onchange='window.location.href=...'` with values that could be manipulated. This inline event handler pattern bypasses Content Security Policy protections.

**Fix:** Remove inline event handlers. Use Drupal's Form API with AJAX callbacks, or attach JavaScript behaviors via `Drupal.behaviors` with data attributes.

---

### 9. Devel Module Present — 🔶 Production Deployment

**Type:** Information Disclosure
**File:** `web/modules/contrib/devel/`

The Devel module is installed. This module exposes debugging tools including PHP evaluation, database queries, and detailed error messages. If it remains enabled in production, it gives attackers detailed system information and potential code execution.

**Fix:** Ensure Devel is disabled and uninstalled before deploying to production. Add a deployment checklist item to verify. Consider using `composer install --no-dev` for production builds.

---

## MEDIUM Findings

### 10. Development Services Configuration Active — 🔶 Production Deployment

**Type:** Information Disclosure
**File:** `web/sites/development.services.yml`

This file disables caching (`cache.backend.null`) and enables debug cacheability headers (`http.response.debug_cacheability_headers: true`). If loaded in production, it degrades performance and exposes internal caching metadata in HTTP response headers.

**Fix:** Ensure `development.services.yml` is only loaded in local environments. Verify that `settings.local.php` (which typically includes this) is not deployed to production.

---

### 11. Stored XSS via Unescaped User Input in Notes Fields — ⚠️ Still Needed

**Type:** Stored XSS
**Files:** `TournamentDepositForfeitForm.php` (line 169), `TournamentDepositRefundForm.php` (line 254)

User-provided "reason" text from form textareas is concatenated directly into notes fields stored in the database without sanitization. When these notes are later displayed, embedded HTML/JavaScript could execute.

**Fix:** Sanitize before storage:

```php
$reason = htmlspecialchars($form_state->getValue('reason'), ENT_QUOTES, 'UTF-8');
```

---

### 12. Error Messages Expose System Details — ⚠️ Still Needed

**Type:** Information Disclosure
**Files:** Multiple controllers and services

Exception messages are returned directly in JSON responses (e.g., `'error' => 'Failed: ' . $e->getMessage()`). These can reveal database schema, file paths, and internal architecture to attackers.

**Fix:** Log details server-side, return generic messages to clients:

```php
\Drupal::logger('ccsoccer')->error('Operation failed: @error', ['@error' => $e->getMessage()]);
return new JsonResponse(['error' => 'An unexpected error occurred.'], 500);
```

---

### 13. Missing Rate Limiting on Sensitive Operations — ✅ Completed March 16, 2026

**Type:** Denial of Service
**Files:** AJAX endpoints (skill updates, roster moves, notification sending)

No rate limiting exists on AJAX endpoints that modify data or send notifications. An attacker could flood these endpoints to corrupt data or send mass notifications.

**Fix:** Use Drupal's flood control:

```php
$flood = \Drupal::flood();
if (!$flood->isAllowed('ccsoccer_ajax', 50, 300)) {
  return new JsonResponse(['error' => 'Too many requests'], 429);
}
$flood->register('ccsoccer_ajax');
```

---

### 14. Missing Content Security Policy Headers — 🔶 Production Deployment

**Type:** Security Misconfiguration
**File:** `web/.htaccess`

No Content Security Policy (CSP) headers are configured. Without CSP, the site has no browser-level protection against XSS, clickjacking, or unauthorized script loading.

**Fix:** Add CSP headers (start with report-only to avoid breaking things):

```apache
Header set Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
```

---

### 15. Inconsistent HTML Escaping in Render Arrays — ⚠️ Still Needed

**Type:** XSS
**Files:** `GameStatusForm.php`, `RosterBuilderForm.php`, `OverrideController.php`, `WaitlistController.php`

The codebase inconsistently uses `htmlspecialchars()` when building `#markup` render arrays. Some user-controlled values are escaped while others in the same file are not.

**Fix:** Standardize on `\Drupal\Component\Utility\Html::escape()` for all user-controlled data. Prefer Twig templates with auto-escaping over `#markup` strings.

---

## LOW Findings

### 16. Entity Access Not Validated on Clone Parameters — ⚠️ Still Needed

**Type:** Parameter Tampering
**Files:** `SeasonForm.php`, `TournamentForm.php`

The `clone_from` query parameter loads entities by ID without validating that the current user has access to the source entity. This could allow unauthorized users to read configuration of seasons or tournaments they shouldn't access.

**Fix:**

```php
$source = Season::load($clone_id);
if (!$source || !$source->access('view')) {
  throw new AccessDeniedException();
}
```

---

## Prioritized Action Plan

### ✅ Completed (March 16, 2026 — Code Fixes)

1. ~~Add `X-CSRF-Token` headers to all AJAX POST requests~~ — `fetchWithCsrf()` wrapper added to 8 JS files (player-skill, season-players, notification-confirm, tournament-teams, roster-builder, tournament-roster-builder, schedule-builder, tournament-schedule-builder)
2. ~~Add controller-level access checks to all AJAX endpoints~~ — `hasPermission()` checks added to 8 controllers (PlayerAdminController, PlayerSkillController, TournamentTeamsController, RosterBuilderController, TournamentRosterBuilderController, ScheduleController, TournamentScheduleController, NotificationController)
3. ~~Replace `innerHTML` usage with safe DOM methods~~ — Converted to `textContent`/`createElement`/`appendChild` in 4 JS files (group-management, schedule-builder, tournament-roster-builder, tournament-schedule-builder); `escapeHtml()` applied to all server values in snapshot tables
4. ~~Implement rate limiting on player-facing AJAX endpoints~~ — Drupal flood control added to GroupController (userSearch: 30/60s, invite: 10/5min, nudge: 5/10min); admin endpoints not rate-limited (trusted users behind permission checks)

### 🔶 Before Production Deployment (Do at Launch)

1. Remove/rotate the Authorize.net API credentials from version control; move to environment variables or `settings.local.php` config overrides *(Finding #1)*
2. Delete the SQL database dump (`ccsoccer-d11-migrated-200users.sql`) from the repository *(Finding #2 — do this now)*
3. Set a strong `hash_salt` value in production `settings.local.php` *(Finding #3)*
4. Disable and uninstall the Devel module for production *(Finding #9)*
5. Configure `trusted_host_patterns` — do on test server now, then again for production *(Finding #4)*
6. Verify `development.services.yml` is NOT loaded in production *(Finding #10)*
7. Add Content Security Policy headers (start with report-only mode) *(Finding #14)*

### ⚠️ Still Needed (Code Fixes)

1. Remove inline JavaScript event handler in `ReportController.php`; use Drupal behaviors *(Finding #8 — HIGH)*
2. Sanitize "reason" text before storage in `TournamentDepositForfeitForm.php` and `TournamentDepositRefundForm.php` *(Finding #11 — MEDIUM)*
3. Replace detailed `$e->getMessage()` in JSON responses with generic error messages; log details server-side *(Finding #12 — MEDIUM)*
4. Standardize HTML escaping across render arrays using `Html::escape()` *(Finding #15 — MEDIUM)*
5. Validate entity access on `clone_from` parameter in `SeasonForm.php` and `TournamentForm.php` *(Finding #16 — LOW)*
