Issues (26)

Security Analysis    no vulnerabilities found

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  Header Injection
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

scripts/background_tasks___handler.php (1 issue)

Severity
1
<?php
2
/**
3
 * Teampass - a collaborative passwords manager.
4
 * ---
5
 * This file is part of the TeamPass project.
6
 * 
7
 * TeamPass is free software: you can redistribute it and/or modify it
8
 * under the terms of the GNU General Public License as published by
9
 * the Free Software Foundation, version 3 of the License.
10
 * 
11
 * TeamPass is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU General Public License for more details.
15
 * 
16
 * You should have received a copy of the GNU General Public License
17
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18
 * 
19
 * Certain components of this file may be under different licenses. For
20
 * details, see the `licenses` directory or individual file headers.
21
 * ---
22
 * @file      background_tasks___handler.php
23
 * @author    Nils Laumaillé ([email protected])
24
 * @copyright 2009-2025 Teampass.net
25
 * @license   GPL-3.0
26
 * @see       https://www.teampass.net
27
 */
28
29
use Symfony\Component\Process\Process;
30
use TeampassClasses\ConfigManager\ConfigManager;
31
32
require_once __DIR__.'/../sources/main.functions.php';
33
require_once __DIR__ . '/taskLogger.php';
34
35
class BackgroundTasksHandler {
36
    private $settings;
37
    private $logger;
38
    private $maxParallelTasks;
39
    private $maxExecutionTime;
40
    private $batchSize;
41
    private $maxTimeBeforeRemoval;
42
43
    public function __construct(array $settings) {
44
        $this->settings = $settings;
45
        $this->logger = new TaskLogger($settings, LOG_TASKS_FILE);
46
        $this->maxParallelTasks = $settings['max_parallel_tasks'] ?? 2;
47
        $this->maxExecutionTime = $settings['task_maximum_run_time'] ?? 600;
48
        $this->batchSize = $settings['task_batch_size'] ?? 50;
49
        $this->maxTimeBeforeRemoval = isset($settings['history_duration']) ? ($settings['history_duration'] * 24 * 3600) : (15 * 24 * 3600);
50
    }
51
52
    /**
53
     * Main function to process background tasks
54
     */
55
    public function processBackgroundTasks() {
56
        // Prevent multiple concurrent executions
57
        if (!$this->acquireProcessLock()) {
58
            if (LOG_TASKS=== true) $this->logger->log('Process already running', 'INFO');
59
            return false;
60
        }
61
62
        try {
63
            $this->cleanupStaleTasks();
64
            $this->processTaskBatches();
65
            $this->performMaintenanceTasks();
66
        } catch (Exception $e) {
67
            if (LOG_TASKS=== true) $this->logger->log('Task processing error: ' . $e->getMessage(), 'ERROR');
68
        } finally {
69
            $this->releaseProcessLock();
70
        }
71
    }
72
73
    /**
74
     * Acquire a lock to prevent multiple instances of this script from running simultaneously.
75
     * @return bool
76
     */
77
    private function acquireProcessLock(): bool {
78
        $lockFile = empty(TASKS_LOCK_FILE) ? __DIR__.'/../files/teampass_background_tasks.lock' : TASKS_LOCK_FILE;
79
        
80
        $fp = fopen($lockFile, 'w');
81
        
82
        if (!flock($fp, LOCK_EX | LOCK_NB)) {
83
            return false;
84
        }
85
        
86
        fwrite($fp, (string)getmypid());
87
        return true;
88
    }
89
90
    /**
91
     * Release the lock file.
92
     */
93
    private function releaseProcessLock() {
94
        $lockFile = empty(TASKS_LOCK_FILE) ? __DIR__.'/../files/teampass_background_tasks.lock' : TASKS_LOCK_FILE;
95
        unlink($lockFile);
96
    }
97
98
    /**
99
     * Cleanup stale tasks that have been running for too long or are marked as failed.
100
     */
101
    private function cleanupStaleTasks() {
102
        // Mark tasks as failed if they've been running too long
103
        DB::query(
104
            'UPDATE ' . prefixTable('background_tasks') . ' 
105
            SET is_in_progress = -1, 
106
                finished_at = %i, 
107
                status = "failed",
108
                error_message = "Task exceeded maximum execution time of '.$this->maxExecutionTime.' seconds"
109
            WHERE is_in_progress = 1 
110
            AND updated_at < %i',
111
            time(),
112
            time() - $this->maxExecutionTime
113
        );
114
115
        // Remove very old failed tasks
116
        DB::query(
117
            'DELETE t, st FROM ' . prefixTable('background_tasks') . ' t
118
            INNER JOIN ' . prefixTable('background_subtasks') . ' st ON (t.increment_id = st.task_id)
119
            WHERE t.finished_at < %i 
120
            AND t.status = %s',
121
            time() - $this->maxTimeBeforeRemoval,
122
            "failed"
123
        );
124
    }
125
126
    /**
127
     * Process batches of tasks.
128
     * This method fetches tasks from the database and processes them in parallel.
129
     */
130
    private function processTaskBatches() {
131
        $runningTasks = $this->countRunningTasks();
132
        
133
        // Check if the maximum number of parallel tasks is reached
134
        if ($runningTasks >= $this->maxParallelTasks) {
135
            if (LOG_TASKS=== true) $this->logger->log('Wait ... '.$runningTasks.' out of '.$this->maxParallelTasks.' are already running ', 'INFO');
136
            return;
137
        }
138
139
        $availableSlotsCount = $this->maxParallelTasks - $runningTasks;
140
141
        // Fetch next batch of tasks
142
        $tasks = DB::query(
143
            'SELECT increment_id, process_type, arguments 
144
            FROM ' . prefixTable('background_tasks') . '
145
            WHERE is_in_progress = 0 
146
            AND (finished_at IS NULL OR finished_at = "")
147
            ORDER BY increment_id ASC
148
            LIMIT %i',
149
            min($this->batchSize, $availableSlotsCount)
150
        );
151
152
        foreach ($tasks as $task) {
153
            if (LOG_TASKS=== true) $this->logger->log('Launching '.$task['increment_id'], 'INFO');
154
            $this->processIndividualTask($task);
155
        }
156
    }
157
158
    /**
159
     * Process an individual task.
160
     * This method updates the task status in the database and starts a new process for the task.
161
     * @param array $task The task to process.
162
     */
163
    private function processIndividualTask(array $task) {
164
        if (LOG_TASKS=== true)  $this->logger->log('Starting task: ' . print_r($task, true), 'INFO');
165
166
        // Store progress in the database        
167
        DB::update(
168
            prefixTable('background_tasks'),
169
            [
170
                'is_in_progress' => 1,
171
                'started_at' => time(),
172
                'updated_at' => time(),
173
                'status' => 'in_progress'
174
            ],
175
            'increment_id = %i',
176
            $task['increment_id']
177
        );
178
179
        // Prepare process
180
        $process = new Process([
181
            PHP_BINARY,
182
            __DIR__ . '/background_tasks___worker.php',
183
            $task['increment_id'],
184
            $task['process_type'],
185
            $task['arguments']
186
        ]);
187
188
        // Launch process
189
        try{
190
            $process->setTimeout($this->maxExecutionTime);
191
            $process->mustRun();
192
193
        } catch (Exception $e) {
194
            if (LOG_TASKS=== true) $this->logger->log('Error launching task: ' . $e->getMessage(), 'ERROR');
195
            DB::update(
196
                prefixTable('background_tasks'),
197
                [
198
                    'is_in_progress' => -1,
199
                    'finished_at' => time(),
200
                    'status' => 'failed',
201
                    'error_message' => is_null($e->getMessage()) ? 'Unknown error' : $e->getMessage()
0 ignored issues
show
The condition is_null($e->getMessage()) is always false.
Loading history...
202
                ],
203
                'increment_id = %i',
204
                $task['increment_id']
205
            );
206
        }
207
    }
208
209
    /**
210
     * Count the number of currently running tasks.
211
     * @return int The number of running tasks.
212
     */
213
    private function countRunningTasks(): int {
214
        return DB::queryFirstField(
215
            'SELECT COUNT(*) 
216
            FROM ' . prefixTable('background_tasks') . ' 
217
            WHERE is_in_progress = 1'
218
        );
219
    }
220
221
    /**
222
     * Perform maintenance tasks.
223
     * This method cleans up old items, expired tokens, and finished tasks.
224
     */
225
    private function performMaintenanceTasks() {
226
        $this->cleanMultipleItemsEdition();
227
        $this->handleItemTokensExpiration();
228
        $this->cleanOldFinishedTasks();
229
    }
230
231
    /**
232
     * Clean up multiple items edition.
233
     * This method removes duplicate entries in the items_edition table.
234
     */
235
    private function cleanMultipleItemsEdition() {
236
        DB::query(
237
            'DELETE i1 FROM ' . prefixTable('items_edition') . ' i1
238
            JOIN (
239
                SELECT user_id, item_id, MIN(timestamp) AS oldest_timestamp
240
                FROM ' . prefixTable('items_edition') . '
241
                GROUP BY user_id, item_id
242
            ) i2 ON i1.user_id = i2.user_id AND i1.item_id = i2.item_id
243
            WHERE i1.timestamp > i2.oldest_timestamp'
244
        );
245
    }
246
247
    /**
248
     * Handle item tokens expiration.
249
     * This method removes expired tokens from the items_edition table.
250
     */
251
    private function handleItemTokensExpiration() {
252
        DB::query(
253
            'DELETE FROM ' . prefixTable('items_edition') . '
254
            WHERE timestamp < %i',
255
            time() - ($this->settings['delay_item_edition'] * 60 ?: EDITION_LOCK_PERIOD)
256
        );
257
    }
258
259
    /**
260
     * Clean up old finished tasks.
261
     * This method removes tasks that have been completed for too long.
262
     */
263
    private function cleanOldFinishedTasks() {
264
        // Timestamp cutoff for removal
265
        $cutoffTimestamp = time() - $this->maxTimeBeforeRemoval;
266
    
267
        // 1. Get all finished tasks older than the cutoff timestamp
268
        //    and that are not in progress
269
        $tasks = DB::query(
270
            'SELECT increment_id FROM ' . prefixTable('background_tasks') . '
271
            WHERE status = %s AND is_in_progress = %i AND finished_at < %s',
272
            'completed',
273
            -1,
274
            $cutoffTimestamp
275
        );
276
        
277
        if (empty($tasks)) {
278
            return;
279
        }
280
    
281
        $taskIds = array_column($tasks, 'increment_id');
282
    
283
        // 2. Delete all subtasks related to these tasks
284
        DB::query(
285
            'DELETE FROM ' . prefixTable('background_subtasks') . '
286
            WHERE task_id IN %ls',
287
            $taskIds
288
        );
289
    
290
        // 3. Delete the tasks themselves
291
        DB::query(
292
            'DELETE FROM ' . prefixTable('background_tasks') . '
293
            WHERE increment_id IN %ls',
294
            $taskIds
295
        );
296
    
297
        if (LOG_TASKS=== true) $this->logger->log('Old finished tasks cleaned: ' . count($taskIds), 'INFO');
298
    }
299
}
300
301
302
303
// Main execution
304
try {
305
    $configManager = new ConfigManager();
306
    $settings = $configManager->getAllSettings();
307
    
308
    $tasksHandler = new BackgroundTasksHandler($settings);
309
    $tasksHandler->processBackgroundTasks();
310
} catch (Exception $e) {
311
    error_log('Teampass Background Tasks Error: ' . $e->getMessage());
312
}