Summary
The CSV import wizard keeps its state in a single unqualified session slot, $_SESSION['contactcsvimport']. Two import runs by the same user overwrite each other's state.
Evidence
All usages in program/actions/contacts/import.php share one global key:
$_SESSION['contactcsvimport']['params']
$_SESSION['contactcsvimport']['files']
$_SESSION['contactcsvimport']['map']
$_SESSION['contactcsvimport']
slot carries a per-run identifier: NO
Verified on current master with PHP 8.5.10.
Impact
Low, data integrity only, and confined to the user's own address book — there is no cross-user effect. The import is a multi-step wizard (upload, then column mapping, then commit), so the state lives across several requests. Starting a second import in another tab before finishing the first replaces files and params, and the first wizard then commits against the wrong data or fails.
Suggested fix
Key the slot per import run, the way compose does it with compose_data_<id>:
$_SESSION['contactcsvimport_' . $import_id]
with $import_id generated at upload time and carried through the wizard steps, plus cleanup on completion.
Summary
The CSV import wizard keeps its state in a single unqualified session slot,
$_SESSION['contactcsvimport']. Two import runs by the same user overwrite each other's state.Evidence
All usages in
program/actions/contacts/import.phpshare one global key:Verified on current
masterwith PHP 8.5.10.Impact
Low, data integrity only, and confined to the user's own address book — there is no cross-user effect. The import is a multi-step wizard (upload, then column mapping, then commit), so the state lives across several requests. Starting a second import in another tab before finishing the first replaces
filesandparams, and the first wizard then commits against the wrong data or fails.Suggested fix
Key the slot per import run, the way compose does it with
compose_data_<id>:with
$import_idgenerated at upload time and carried through the wizard steps, plus cleanup on completion.