Issues (22)

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
        if (file_exists($lockFile)) {
96
            unlink($lockFile);
97
        }
98
    }
99
100
    /**
101
     * Cleanup stale tasks that have been running for too long or are marked as failed.
102
     */
103
    private function cleanupStaleTasks() {
104
        // Mark tasks as failed if they've been running too long
105
        DB::query(
106
            'UPDATE ' . prefixTable('background_tasks') . ' 
107
            SET is_in_progress = -1, 
108
                finished_at = %i, 
109
                status = "failed",
110
                error_message = "Task exceeded maximum execution time of '.$this->maxExecutionTime.' seconds"
111
            WHERE is_in_progress = 1 
112
            AND updated_at < %i',
113
            time(),
114
            time() - $this->maxExecutionTime
115
        );
116
117
        // Remove very old failed tasks
118
        DB::query(
119
            'DELETE t, st FROM ' . prefixTable('background_tasks') . ' t
120
            INNER JOIN ' . prefixTable('background_subtasks') . ' st ON (t.increment_id = st.task_id)
121
            WHERE t.finished_at < %i 
122
            AND t.status = %s',
123
            time() - $this->maxTimeBeforeRemoval,
124
            "failed"
125
        );
126
    }
127
128
    /**
129
     * Process batches of tasks.
130
     * This method fetches tasks from the database and processes them in parallel.
131
     */
132
    private function processTaskBatches() {
133
        $runningTasks = $this->countRunningTasks();
134
        
135
        // Check if the maximum number of parallel tasks is reached
136
        if ($runningTasks >= $this->maxParallelTasks) {
137
            if (LOG_TASKS=== true) $this->logger->log('Wait ... '.$runningTasks.' out of '.$this->maxParallelTasks.' are already running ', 'INFO');
138
            return;
139
        }
140
141
        $availableSlotsCount = $this->maxParallelTasks - $runningTasks;
142
143
        // Fetch next batch of tasks
144
        $tasks = DB::query(
145
            'SELECT increment_id, process_type, arguments 
146
            FROM ' . prefixTable('background_tasks') . '
147
            WHERE is_in_progress = 0 
148
            AND (finished_at IS NULL OR finished_at = "")
149
            ORDER BY increment_id ASC
150
            LIMIT %i',
151
            min($this->batchSize, $availableSlotsCount)
152
        );
153
154
        foreach ($tasks as $task) {
155
            if (LOG_TASKS=== true) $this->logger->log('Launching '.$task['increment_id'], 'INFO');
156
            $this->processIndividualTask($task);
157
        }
158
    }
159
160
    /**
161
     * Process an individual task.
162
     * This method updates the task status in the database and starts a new process for the task.
163
     * @param array $task The task to process.
164
     */
165
    private function processIndividualTask(array $task) {
166
        if (LOG_TASKS=== true)  $this->logger->log('Starting task: ' . print_r($task, true), 'INFO');
167
168
        // Store progress in the database        
169
        DB::update(
170
            prefixTable('background_tasks'),
171
            [
172
                'is_in_progress' => 1,
173
                'started_at' => time(),
174
                'updated_at' => time(),
175
                'status' => 'in_progress'
176
            ],
177
            'increment_id = %i',
178
            $task['increment_id']
179
        );
180
181
        // Prepare process
182
        $process = new Process([
183
            PHP_BINARY,
184
            __DIR__ . '/background_tasks___worker.php',
185
            $task['increment_id'],
186
            $task['process_type'],
187
            $task['arguments']
188
        ]);
189
190
        // Launch process
191
        try{
192
            $process->setTimeout($this->maxExecutionTime);
193
            $process->mustRun();
194
195
        } catch (Exception $e) {
196
            if (LOG_TASKS=== true) $this->logger->log('Error launching task: ' . $e->getMessage(), 'ERROR');
197
            DB::update(
198
                prefixTable('background_tasks'),
199
                [
200
                    'is_in_progress' => -1,
201
                    'finished_at' => time(),
202
                    'status' => 'failed',
203
                    '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...
204
                ],
205
                'increment_id = %i',
206
                $task['increment_id']
207
            );
208
        }
209
    }
210
211
    /**
212
     * Count the number of currently running tasks.
213
     * @return int The number of running tasks.
214
     */
215
    private function countRunningTasks(): int {
216
        return DB::queryFirstField(
217
            'SELECT COUNT(*) 
218
            FROM ' . prefixTable('background_tasks') . ' 
219
            WHERE is_in_progress = 1'
220
        );
221
    }
222
223
    /**
224
     * Perform maintenance tasks.
225
     * This method cleans up old items, expired tokens, and finished tasks.
226
     */
227
    private function performMaintenanceTasks() {
228
        $this->cleanMultipleItemsEdition();
229
        $this->handleItemTokensExpiration();
230
        $this->cleanOldFinishedTasks();
231
    }
232
233
    /**
234
     * Clean up multiple items edition.
235
     * This method removes duplicate entries in the items_edition table.
236
     */
237
    private function cleanMultipleItemsEdition() {
238
        DB::query(
239
            'DELETE i1 FROM ' . prefixTable('items_edition') . ' i1
240
            JOIN (
241
                SELECT user_id, item_id, MIN(timestamp) AS oldest_timestamp
242
                FROM ' . prefixTable('items_edition') . '
243
                GROUP BY user_id, item_id
244
            ) i2 ON i1.user_id = i2.user_id AND i1.item_id = i2.item_id
245
            WHERE i1.timestamp > i2.oldest_timestamp'
246
        );
247
    }
248
249
    /**
250
     * Handle item tokens expiration.
251
     * This method removes expired tokens from the items_edition table.
252
     */
253
    private function handleItemTokensExpiration() {
254
        DB::query(
255
            'DELETE FROM ' . prefixTable('items_edition') . '
256
            WHERE timestamp < %i',
257
            time() - ($this->settings['delay_item_edition'] * 60 ?: EDITION_LOCK_PERIOD)
258
        );
259
    }
260
261
    /**
262
     * Clean up old finished tasks.
263
     * This method removes tasks that have been completed for too long.
264
     */
265
    private function cleanOldFinishedTasks() {
266
        // Timestamp cutoff for removal
267
        $cutoffTimestamp = time() - $this->maxTimeBeforeRemoval;
268
    
269
        // 1. Get all finished tasks older than the cutoff timestamp
270
        //    and that are not in progress
271
        $tasks = DB::query(
272
            'SELECT increment_id FROM ' . prefixTable('background_tasks') . '
273
            WHERE status = %s AND is_in_progress = %i AND finished_at < %s',
274
            'completed',
275
            -1,
276
            $cutoffTimestamp
277
        );
278
        
279
        if (empty($tasks)) {
280
            return;
281
        }
282
    
283
        $taskIds = array_column($tasks, 'increment_id');
284
    
285
        // 2. Delete all subtasks related to these tasks
286
        DB::query(
287
            'DELETE FROM ' . prefixTable('background_subtasks') . '
288
            WHERE task_id IN %ls',
289
            $taskIds
290
        );
291
    
292
        // 3. Delete the tasks themselves
293
        DB::query(
294
            'DELETE FROM ' . prefixTable('background_tasks') . '
295
            WHERE increment_id IN %ls',
296
            $taskIds
297
        );
298
    
299
        if (LOG_TASKS=== true) $this->logger->log('Old finished tasks cleaned: ' . count($taskIds), 'INFO');
300
    }
301
}
302
303
304
305
// Main execution
306
try {
307
    $configManager = new ConfigManager();
308
    $settings = $configManager->getAllSettings();
309
    
310
    $tasksHandler = new BackgroundTasksHandler($settings);
311
    $tasksHandler->processBackgroundTasks();
312
} catch (Exception $e) {
313
    error_log('Teampass Background Tasks Error: ' . $e->getMessage());
314
}