Security sweep 2: API archive, unarchive, resolve and close now check the client named in the call the same as create, update and delete do (every call except read.php and time.php counts as a write); API asset, contact, network and document create/update keep linked contacts, locations, vendors, networks and folders within the record's own client; linking a contact to an asset refuses another client's contact, and unlinking only clears the contact it was linked to; agents limited to certain clients no longer see other clients' names in the asset import template file name, export filter summaries, or the messages from file moves, unlinks, ticket asset removal, bulk project add and bulk interface network; guest quote Accept and Decline post instead of being plain links and check the key with hash_equals; exports that match nothing say so instead of showing a blank page; about 30 handlers now stop with 'no longer exists' when their record was just deleted, instead of showing warnings or an error page and writing history for a record that is gone; restore_contact_note reads its own id parameter; ticket prefix pattern escaped for the v flag; missing user agent and array key parameters handled; CHANGELOG updated

This commit is contained in:
johnnyq
2026-10-26 19:54:36 -04:00
parent 5dbd497c31
commit 01fbcda75d
45 changed files with 373 additions and 67 deletions

View File

@@ -159,3 +159,9 @@ if (isset($_POST['asset_network_id'])) {
} else {
$network = '0';
}
// Only the client's own, as in the asset form - another client's would show on this one's asset, and the asset on theirs
$vendor = assetClientOwnedId('vendor', $vendor, $client_id);
$location = assetClientOwnedId('location', $location, $client_id);
$contact = assetClientOwnedId('contact', $contact, $client_id);
$network = assetClientOwnedId('network', $network, $client_id);

View File

@@ -105,3 +105,6 @@ if (isset($_POST['contact_location_id'])) {
} else {
$location_id = '0';
}
// Only one of the client's own sites, as in the contact form
$location_id = assetClientOwnedId('location', $location_id, $client_id);

View File

@@ -41,3 +41,6 @@ if (isset($_POST['document_folder_id'])) {
} else {
$folder = 0;
}
// Only one of the client's own folders, as in the document form
$folder = folderOwnedId($folder, $client_id);

View File

@@ -200,7 +200,10 @@ if ($resource_module[$resource] !== '' && lookupUserPermission($resource_module[
// permission at all, just an optional filter applied inside apiClientScopeSql().
$client_id = intval($_POST['client_id'] ?? $_GET['client_id'] ?? 0);
$client_id_supplied = isset($_POST['client_id']) || isset($_GET['client_id']);
$is_write = in_array($operation_file, ['create.php', 'update.php', 'delete.php'], true);
// Everything but a read is a write - archive, unarchive, resolve and close act on one
// client's record just as update does, so they name a client the user can reach too.
// Listing the reads, not the writes, keeps a new kind of write from slipping past.
$is_write = !in_array($operation_file, ['read.php', 'time.php'], true);
if ($is_write && !apiUserCanAccessClient($client_id)) {
// Writes act on a single client the caller names (client_id 0 = a global record).
// The user must be able to access it - this also blocks a restricted user from

View File

@@ -80,4 +80,7 @@ if (isset($_POST['network_location_id'])) {
$location_id = intval($network_row['network_location_id']);
} else {
$location_id = 0;
}
}
// Only one of the client's own sites, as in the network form
$location_id = assetClientOwnedId('location', $location_id, $client_id);

View File

@@ -22,7 +22,7 @@ $_POST = json_decode(file_get_contents('php://input'), true);
// Get IP & UA
$ip = escapeSql(getIP());
$user_agent = escapeSql($_SERVER['HTTP_USER_AGENT']);
$user_agent = escapeSql($_SERVER['HTTP_USER_AGENT'] ?? '');
// Temp Added this to work with the new logAction function
$session_ip = $ip;