Well, I'm getting around to some testing and coding this weekend.
I have the following plans for the next two weeks.
Context
Tickets CAD is a 30-year-old PHP Computer Aided Dispatch system with 808 PHP files and ~250K lines of code. It has critical security vulnerabilities (SQL injection in 353+ files, XSS, MD5 passwords, no CSRF), uses deprecated mysql_* functions via a compatibility shim, and runs on PHP 7.4+. The goal is to modernize the PHP codebase while maintaining backward compatibility for existing installations on diverse platforms (Raspberry Pi, XAMPP/Windows, Docker, Linux LAMP).
The existing installer (install.php) already supports clean install and non-destructive upgrade modes with version tracking in the settings table. We'll build on this.
Prerequisites & Testing Setup
What We Need
- XAMPP on this Windows workstation - for local testing (PHP 8.x + MySQL/MariaDB)
- WSL - already available, for Linux testing
- The cloned repository - already at tickets/
Testing Strategy
- Install XAMPP and configure it to serve the tickets/ directory
- Import DB_FULL.sql to create a test database
- Run the installer to verify baseline functionality works
- After each batch of changes, verify the app loads, login works, and core dispatch functions operate
- We cannot run automated tests (no test suite exists) — we'll test manually via the browser and verify SQL syntax with PHP lint
- We'll add a php -l lint check on all modified files before each commit
Upgrade Path Design
- The existing install.php already handles upgrades non-destructively (adds missing tables/columns, never drops data)
- We'll increment the version in incs/versions.inc.php for each release
- All our PHP changes are backward-compatible with the same database schema — we're changing HOW queries are built, not WHAT data they access
- No database schema changes needed for security fixes (Phase 1)
- For inexperienced admins: upgrade = download new files, visit /install.php, select "Upgrade", done
- Docker users: pull new image, restart container
- XAMPP users: extract new files over old ones, visit /install.php
Branch Strategy
Create branch security-modernization from main in the tickets repo.
Implementation Order
Step 1: Create Database Abstraction Layer
Files to create: incs/db.inc.php
Create a thin wrapper around mysqli that provides:
- A db() function returning a singleton mysqli connection
- A db_query($sql, $params, $types) function for prepared statements
- A db_fetch_all($sql, $params, $types) convenience function
- A db_fetch_one($sql, $params, $types) convenience function
- A db_escape($string) function for cases where prepared statements can't be used (dynamic table names)
- A db_insert_id() function
- A db_affected_rows() function
This layer will coexist with the mysql2i shim. Files migrate incrementally — old code keeps working, new code uses the new layer. The shim stays until all files are migrated.
Why first: Every subsequent security fix depends on this layer.
Step 2: Add Security Helper Functions
Files to create/modify: incs/security.inc.php, incs/functions.inc.php
Create helper functions:
- e($string) → htmlspecialchars($string, ENT_QUOTES, 'UTF-8') for XSS prevention (already exists in install.php as h(), standardize it)
- csrf_token() → generate and store CSRF token in session
- csrf_verify() → validate CSRF token from form submission
- sanitize_int($val) → (int)$val with null handling
- sanitize_string($val) → trim + basic cleanup
Add require_once 'security.inc.php' to functions.inc.php.
Step 3: Fix Critical SQL Injection — AJAX Endpoints (155 files)
Directory: ajax/
These are the highest-risk files since they accept user input directly. Work through them alphabetically in batches of ~15-20 files per commit:
- Replace $_GET/$_POST variables directly in SQL strings with prepared statements via db_query()
- Replace addslashes() with prepared statement parameters
- Replace quote_smart() with prepared statement parameters
- Replace mysql_real_escape_string() with prepared statement parameters
- Keep $GLOBALS['mysql_prefix'] for table names (these are from config, not user input)
Commit pattern: "Fix SQL injection in ajax/ batch N (files X-Y)"
Step 4: Fix SQL Injection — Core Include Files
Files: incs/functions.inc.php (5281 lines), incs/functions_major.inc.php (1745 lines), incs/functions_nm.inc.php, incs/functions_major_nm.inc.php, incs/config.inc.php, incs/messaging.inc.php, incs/member.inc.php, incs/login.inc.php
These are the shared libraries called by everything else. Migrate all query functions to use prepared statements.
Step 5: Fix SQL Injection — Root PHP Files (~200 files)
Directory: Root *.php files
Work through alphabetically in batches. Same pattern as Step 3.
Step 6: Fix SQL Injection — Mobile & Portal
Directories: rm/, portal/
Same pattern. ~63 files total.
Step 7: XSS Prevention
All files that output user data to HTML:
- Add e() calls around all $_GET, $_POST, $_SESSION, and database-sourced values in HTML output
- Focus on form values, error messages, and displayed user content
- Add Content-Type: application/json headers to all AJAX responses returning JSON
Step 8: Password Hashing Migration
Files: incs/login.inc.php, incs/functions.inc.php, any file that sets passwords
- Login already supports password_verify() with MD5 fallback (done in recent hardening)
- Add automatic rehashing: when user logs in with MD5 password, rehash with password_hash() and update DB
- Update all password-setting code (user creation, password change) to use password_hash()
- Remove MD5 password hashing from all new user creation paths
Step 9: CSRF Protection
All state-changing forms:
- Add csrf_token() to all forms as a hidden field
- Add csrf_verify() check at the top of all form processing scripts
- AJAX endpoints: add CSRF token to AJAX request headers
Step 10: Security Headers
File: incs/functions.inc.php (in the initialization section)
Add headers to the page initialization:
php
header('X-Frame-Options: SAMEORIGIN');
header('X-Content-Type-Options: nosniff');
header('X-XSS-Protection: 1; mode=block');
Step 11: Update Version & Upgrade Documentation
- Bump version in incs/versions.inc.php
- Update INSTALL.txt and get_started.txt with upgrade instructions
- Add UPGRADING.md with clear step-by-step instructions for:
- XAMPP/Windows users
- Linux LAMP users
- Raspberry Pi users
- Docker users
- Add CHANGELOG.md documenting all security fixes
Commit Strategy
- Each "step" above will have multiple commits (one per batch of files)
- Every commit message describes WHAT was changed and WHY
- Format: [security] Fix SQL injection in ajax/form_post.php, ajax/get_assigns.php, ...
- PHP lint check (php -l) on all modified files before committing
- Test core functionality (login, create ticket, dispatch) after each step
Upgrade Path for End Users
For All Users (XAMPP, Linux, Raspberry Pi)
- Back up your database (mysqldump)
- Back up your incs/mysql.inc.php (contains your DB credentials)
- Download/extract new files over old ones
- Visit http://yourserver/tickets/install.php
- Select "Upgrade" mode
- Your data, settings, and configuration are preserved
- Login and verify everything works
For Docker Users
- Pull new image
- Restart container
- Visit install.php and run upgrade
- Database data persists in volume
What Makes This Safe
- No database schema changes in Phase 1 (security fixes only change PHP code, not DB structure)
- The mysql2i shim remains in place — if any file was missed, it still works
- install.php upgrade mode is non-destructive (never drops tables or data)
- Version tracking in settings._version ensures upgrades are idempotent
Files Modified (Summary)
Category
File Count
Description
New: incs/db.inc.php
1
Database abstraction layer
New: incs/security.inc.php
1
Security helper functions
Modified: ajax/*.php
~155
SQL injection fixes
Modified: incs/*.php
~15
Core library modernization
Modified: Root *.php
~200
SQL injection + XSS fixes
Modified: rm/*.php
~40
Mobile module fixes
Modified: portal/*.php
~23
Portal module fixes
New: UPGRADING.md
1
Upgrade instructions
New: CHANGELOG.md
1
Change log
What We Are NOT Doing (Yet)
- No framework migration (no Composer, no Laravel — that's Phase 2)
- No database schema changes (that's Phase 3)
- No UI changes (that's Phase 4)
- No Leaflet/jQuery updates (that's Phase 4)
- No REST API (that's Phase 5)
- We keep the mysql2i shim for now — it's removed when ALL files are migrated
Risk Mitigation
- If a file breaks: The mysql2i shim catches old-style calls, so partially-migrated state still works
- If prepared statements fail: The db_query() wrapper will log errors with file/line info
- Rollback: Users can restore from their backup + old files
- Testing: Manual testing after each step; php -l syntax checking on every commit