Add query optimization and reliability improvements
- Consolidate StatsModel queries from 12 to 3 using conditional aggregation - Add input validation to DashboardController (sort columns, dates, priorities) - Combine getCategories/getTypes into single query - Add transaction support to BulkOperationsModel with atomic mode option - Add depth limit (20) to dependency cycle detection to prevent DoS - Add caching to UserModel.getAllGroups() with 5-minute TTL - Improve ticket ID generation with 50 attempts, exponential backoff, and fallback Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -169,6 +169,9 @@ class DependencyModel {
|
||||
return $result;
|
||||
}
|
||||
|
||||
/** Maximum depth for cycle detection to prevent DoS */
|
||||
private const MAX_DEPENDENCY_DEPTH = 20;
|
||||
|
||||
/**
|
||||
* Check if adding a dependency would create a cycle
|
||||
*
|
||||
@@ -177,7 +180,7 @@ class DependencyModel {
|
||||
* @param string $type Dependency type
|
||||
* @return bool True if it would create a cycle
|
||||
*/
|
||||
private function wouldCreateCycle($ticketId, $dependsOnId, $type) {
|
||||
private function wouldCreateCycle($ticketId, $dependsOnId, $type): bool {
|
||||
// Only check for cycles in blocking relationships
|
||||
if (!in_array($type, ['blocks', 'blocked_by'])) {
|
||||
return false;
|
||||
@@ -185,23 +188,39 @@ class DependencyModel {
|
||||
|
||||
// Check if dependsOnId already has ticketId in its dependency chain
|
||||
$visited = [];
|
||||
return $this->hasDependencyPath($dependsOnId, $ticketId, $visited);
|
||||
return $this->hasDependencyPath($dependsOnId, $ticketId, $visited, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there's a dependency path from source to target
|
||||
*
|
||||
* Uses iterative BFS approach with depth limit to prevent stack overflow
|
||||
* and DoS attacks from deeply nested or circular dependencies.
|
||||
*
|
||||
* @param string $source Source ticket ID
|
||||
* @param string $target Target ticket ID
|
||||
* @param array $visited Already visited tickets
|
||||
* @param array $visited Already visited tickets (passed by reference for efficiency)
|
||||
* @param int $depth Current recursion depth
|
||||
* @return bool True if path exists
|
||||
*/
|
||||
private function hasDependencyPath($source, $target, &$visited) {
|
||||
private function hasDependencyPath($source, $target, array &$visited, int $depth): bool {
|
||||
// Depth limit to prevent DoS and stack overflow
|
||||
if ($depth >= self::MAX_DEPENDENCY_DEPTH) {
|
||||
error_log("Dependency cycle detection hit max depth ({$depth}) from {$source} to {$target}");
|
||||
return false; // Assume no cycle to avoid blocking legitimate operations
|
||||
}
|
||||
|
||||
if ($source === $target) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (in_array($source, $visited)) {
|
||||
if (in_array($source, $visited, true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Limit visited array size to prevent memory exhaustion
|
||||
if (count($visited) > 100) {
|
||||
error_log("Dependency cycle detection visited too many nodes from {$source} to {$target}");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -215,7 +234,7 @@ class DependencyModel {
|
||||
$result = $stmt->get_result();
|
||||
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
if ($this->hasDependencyPath($row['depends_on_id'], $target, $visited)) {
|
||||
if ($this->hasDependencyPath($row['depends_on_id'], $target, $visited, $depth + 1)) {
|
||||
$stmt->close();
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user