Verified, high-confidence fixes from a project-wide review: - markdown.js: escape " and ' in the HTML-escape step. User-controlled image/link URLs and alt text were interpolated into "..." attributes without quote escaping, allowing attribute breakout and injected event handlers (stored XSS, only mitigated by CSP). Flagged independently by two reviewers. - cron/create_recurring_tickets.php & cron/cleanup_ratelimit.php: a mangled crontab example inside the docblock contained */ which closed the comment early, causing a fatal parse error — both cron jobs never ran. Rewrote the docblocks without a literal */. - update_ticket.php: validate visibility BEFORE the core DB write so an invalid payload can't leave the ticket updated while the request reports failure (which also skipped the audit delta and stats cache invalidation). - watch_ticket.php: GET watcher_count was capped at 6 (count of a LIMIT 6 list); use an unbounded COUNT(*) so it matches the POST path. - notifications.php: "assigned to me" LIKE pattern lacked a trailing delimiter, so user 12 also matched 120/123/etc.; anchor with }. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
99 lines
2.3 KiB
PHP
99 lines
2.3 KiB
PHP
#!/usr/bin/env php
|
|
<?php
|
|
|
|
/**
|
|
* Rate Limit Cleanup Cron Job
|
|
*
|
|
* Cleans up expired rate limit files from the temp directory.
|
|
* Should be run via cron every 5-10 minutes:
|
|
* 5 * * * * /usr/bin/php /path/to/cron/cleanup_ratelimit.php
|
|
*
|
|
* This script can also be run manually for immediate cleanup.
|
|
*/
|
|
|
|
// Prevent web access
|
|
if (php_sapi_name() !== 'cli') {
|
|
http_response_code(403);
|
|
exit('CLI access only');
|
|
}
|
|
|
|
// Configuration
|
|
$rateLimitDir = sys_get_temp_dir() . '/tinker_tickets_ratelimit';
|
|
$lockFile = $rateLimitDir . '/.cleanup.lock';
|
|
$maxAge = 120; // 2 minutes (2x the rate limit window)
|
|
$maxLockAge = 300; // 5 minutes - release stale locks
|
|
|
|
// Check if directory exists
|
|
if (!is_dir($rateLimitDir)) {
|
|
echo "Rate limit directory does not exist: {$rateLimitDir}\n";
|
|
exit(0);
|
|
}
|
|
|
|
// Acquire lock to prevent concurrent cleanups
|
|
if (file_exists($lockFile)) {
|
|
$lockAge = time() - filemtime($lockFile);
|
|
if ($lockAge < $maxLockAge) {
|
|
echo "Cleanup already in progress (lock age: {$lockAge}s)\n";
|
|
exit(0);
|
|
}
|
|
// Stale lock, remove it
|
|
@unlink($lockFile);
|
|
}
|
|
|
|
// Create lock file
|
|
if (!@touch($lockFile)) {
|
|
echo "Could not create lock file\n";
|
|
exit(1);
|
|
}
|
|
|
|
$now = time();
|
|
$deleted = 0;
|
|
$scanned = 0;
|
|
$errors = 0;
|
|
|
|
try {
|
|
$iterator = new DirectoryIterator($rateLimitDir);
|
|
|
|
foreach ($iterator as $file) {
|
|
if ($file->isDot() || !$file->isFile()) {
|
|
continue;
|
|
}
|
|
|
|
// Skip lock file and non-json files
|
|
$filename = $file->getFilename();
|
|
if ($filename === '.cleanup.lock' || !str_ends_with($filename, '.json')) {
|
|
continue;
|
|
}
|
|
|
|
$scanned++;
|
|
|
|
// Check file age
|
|
$fileAge = $now - $file->getMTime();
|
|
if ($fileAge > $maxAge) {
|
|
$filepath = $file->getPathname();
|
|
if (@unlink($filepath)) {
|
|
$deleted++;
|
|
} else {
|
|
$errors++;
|
|
}
|
|
}
|
|
}
|
|
} catch (Exception $e) {
|
|
echo "Error during cleanup: " . $e->getMessage() . "\n";
|
|
@unlink($lockFile);
|
|
exit(1);
|
|
}
|
|
|
|
// Release lock
|
|
@unlink($lockFile);
|
|
|
|
// Output results
|
|
echo "Rate limit cleanup completed:\n";
|
|
echo " - Scanned: {$scanned} files\n";
|
|
echo " - Deleted: {$deleted} expired files\n";
|
|
if ($errors > 0) {
|
|
echo " - Errors: {$errors} files could not be deleted\n";
|
|
}
|
|
|
|
exit($errors > 0 ? 1 : 0);
|