mirror of
https://github.com/itflow-org/itflow
synced 2026-08-05 07:07:14 +00:00
Claim shared item views atomically and log guest audit IPs
This commit is contained in:
@@ -225,3 +225,24 @@ zgjRYR/zGN5l+az6RB3+0mJRdZdv/y2aRkBlwTxx2gOrPbQAco4a/IOmkE3EbHe7
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Atomically claim one view against a shared item's view limit.
|
||||||
|
// Returns true only if this request won the view; false means the share is
|
||||||
|
// inactive, expired, or out of views. The UPDATE is the claim, so concurrent
|
||||||
|
// requests cannot all pass - call this before any shared content is disclosed.
|
||||||
|
function claimSharedItemView($item_id) {
|
||||||
|
global $mysqli;
|
||||||
|
|
||||||
|
$item_id = intval($item_id);
|
||||||
|
|
||||||
|
mysqli_query($mysqli, "UPDATE shared_items
|
||||||
|
SET item_views = item_views + 1
|
||||||
|
WHERE item_id = $item_id
|
||||||
|
AND item_active = 1
|
||||||
|
AND item_expire_at > NOW()
|
||||||
|
AND (COALESCE(item_view_limit, 0) = 0 OR item_views < item_view_limit)"
|
||||||
|
);
|
||||||
|
|
||||||
|
// -1 (query error) and 0 (limit reached / revoked / expired) both deny
|
||||||
|
return mysqli_affected_rows($mysqli) === 1;
|
||||||
|
}
|
||||||
|
|||||||
@@ -56,6 +56,12 @@ if (isset($_GET['id']) && isset($_GET['key'])) {
|
|||||||
exit("Item cannot be viewed at this time (No file, may have been deleted).");
|
exit("Item cannot be viewed at this time (No file, may have been deleted).");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Claim the view before the file is served. The checks above stay as a
|
||||||
|
// fast path for messaging - this UPDATE is what enforces the limit.
|
||||||
|
if (!claimSharedItemView($item_id)) {
|
||||||
|
exit("Item cannot be viewed at this time (view limit exceeded).");
|
||||||
|
}
|
||||||
|
|
||||||
$file_name = escapeSql($file_row['file_name']);
|
$file_name = escapeSql($file_row['file_name']);
|
||||||
$file_reference_name = escapeSql($file_row['file_reference_name']);
|
$file_reference_name = escapeSql($file_row['file_reference_name']);
|
||||||
$client_id = intval($file_row['file_client_id']);
|
$client_id = intval($file_row['file_client_id']);
|
||||||
@@ -67,10 +73,6 @@ if (isset($_GET['id']) && isset($_GET['key'])) {
|
|||||||
header('Content-Disposition: attachment; filename=' . $file_name);
|
header('Content-Disposition: attachment; filename=' . $file_name);
|
||||||
readfile($file_path);
|
readfile($file_path);
|
||||||
|
|
||||||
// Update file view count
|
|
||||||
$new_item_views = $item_views + 1;
|
|
||||||
mysqli_query($mysqli, "UPDATE shared_items SET item_views = $new_item_views WHERE item_id = $item_id");
|
|
||||||
|
|
||||||
//Logging
|
//Logging
|
||||||
logAudit("Share", "View", "Downloaded shared file $file_name via link", $client_id);
|
logAudit("Share", "View", "Downloaded shared file $file_name via link", $client_id);
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ session_start();
|
|||||||
|
|
||||||
require_once "../includes/inc_set_timezone.php"; // Must be included after session_start to work
|
require_once "../includes/inc_set_timezone.php"; // Must be included after session_start to work
|
||||||
|
|
||||||
|
// logAudit() reads these globals - without them guest audit rows have no IP
|
||||||
|
$session_ip = escapeSql(getIP());
|
||||||
|
$session_user_agent = escapeSql($_SERVER['HTTP_USER_AGENT']);
|
||||||
|
|
||||||
if (isset($_GET['accept_quote'], $_GET['url_key'])) {
|
if (isset($_GET['accept_quote'], $_GET['url_key'])) {
|
||||||
|
|
||||||
$quote_id = intval($_GET['accept_quote']);
|
$quote_id = intval($_GET['accept_quote']);
|
||||||
|
|||||||
@@ -128,6 +128,14 @@ if ($item_type == "Document") {
|
|||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Claim the view before any content is disclosed
|
||||||
|
if (!claimSharedItemView($item_id)) {
|
||||||
|
echo "<div class='alert alert-danger'>Item cannot be viewed at this time. Check with the person that sent you this link to ensure it is correct and has not expired.</div>";
|
||||||
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/footer.php';
|
||||||
|
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
$doc_title = escapeHtml($doc_row['document_name']);
|
$doc_title = escapeHtml($doc_row['document_name']);
|
||||||
$doc_title_escaped = escapeSql($doc_row['document_name']);
|
$doc_title_escaped = escapeSql($doc_row['document_name']);
|
||||||
$doc_content = $purifier->purify($doc_row['document_content']);
|
$doc_content = $purifier->purify($doc_row['document_content']);
|
||||||
@@ -135,10 +143,6 @@ if ($item_type == "Document") {
|
|||||||
echo "<h3>$doc_title</h3>";
|
echo "<h3>$doc_title</h3>";
|
||||||
echo "<div class='prettyContent'>$doc_content</div>";
|
echo "<div class='prettyContent'>$doc_content</div>";
|
||||||
|
|
||||||
// Update document view count
|
|
||||||
$new_item_views = $item_views + 1;
|
|
||||||
mysqli_query($mysqli, "UPDATE shared_items SET item_views = $new_item_views WHERE item_id = $item_id");
|
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
$name = mysqli_real_escape_string($mysqli, $doc_title);
|
$name = mysqli_real_escape_string($mysqli, $doc_title);
|
||||||
logAudit("Share", "View", "Viewed shared $item_type $doc_title_escaped via link", $client_id);
|
logAudit("Share", "View", "Viewed shared $item_type $doc_title_escaped via link", $client_id);
|
||||||
@@ -176,6 +180,14 @@ if ($item_type == "Document") {
|
|||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Claim the view before the credential is decrypted or rendered
|
||||||
|
if (!claimSharedItemView($item_id)) {
|
||||||
|
echo "<div class='alert alert-danger'>Item cannot be viewed at this time. Check with the person that sent you this link to ensure it is correct and has not expired.</div>";
|
||||||
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/footer.php';
|
||||||
|
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
$credential_id = intval($credential_row['credential_id']);
|
$credential_id = intval($credential_row['credential_id']);
|
||||||
$credential_name = escapeHtml($credential_row['credential_name']);
|
$credential_name = escapeHtml($credential_row['credential_name']);
|
||||||
$credential_uri = escapeHtml($credential_row['credential_uri']);
|
$credential_uri = escapeHtml($credential_row['credential_uri']);
|
||||||
@@ -254,10 +266,6 @@ if ($item_type == "Document") {
|
|||||||
|
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
// Update credential view count
|
|
||||||
$new_item_views = $item_views + 1;
|
|
||||||
mysqli_query($mysqli, "UPDATE shared_items SET item_views = $new_item_views WHERE item_id = $item_id");
|
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
$name = escapeSql($credential_row['credential_name']);
|
$name = escapeSql($credential_row['credential_name']);
|
||||||
logAudit("Share", "View", "Viewed shared $item_type $name via link", $client_id);
|
logAudit("Share", "View", "Viewed shared $item_type $name via link", $client_id);
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ $user_agent = escapeSql($_SERVER['HTTP_USER_AGENT']);
|
|||||||
$os = escapeSql(getOS($user_agent));
|
$os = escapeSql(getOS($user_agent));
|
||||||
$browser = escapeSql(getWebBrowser($user_agent));
|
$browser = escapeSql(getWebBrowser($user_agent));
|
||||||
|
|
||||||
|
// logAudit() reads these globals - without them guest audit rows have no IP
|
||||||
|
$session_ip = $ip;
|
||||||
|
$session_user_agent = $user_agent;
|
||||||
|
|
||||||
// Get Company Name
|
// Get Company Name
|
||||||
$sql = mysqli_query($mysqli, "SELECT company_name FROM companies WHERE company_id = 1");
|
$sql = mysqli_query($mysqli, "SELECT company_name FROM companies WHERE company_id = 1");
|
||||||
$row = mysqli_fetch_assoc($sql);
|
$row = mysqli_fetch_assoc($sql);
|
||||||
|
|||||||
143
itflow-share-view-toctou.patch
Normal file
143
itflow-share-view-toctou.patch
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
diff --git a/functions/security.php b/functions/security.php
|
||||||
|
index cda79fc..24c9632 100644
|
||||||
|
--- a/functions/security.php
|
||||||
|
+++ b/functions/security.php
|
||||||
|
@@ -225,3 +225,24 @@ zgjRYR/zGN5l+az6RB3+0mJRdZdv/y2aRkBlwTxx2gOrPbQAco4a/IOmkE3EbHe7
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
+
|
||||||
|
+// Atomically claim one view against a shared item's view limit.
|
||||||
|
+// Returns true only if this request won the view; false means the share is
|
||||||
|
+// inactive, expired, or out of views. The UPDATE is the claim, so concurrent
|
||||||
|
+// requests cannot all pass - call this before any shared content is disclosed.
|
||||||
|
+function claimSharedItemView($item_id) {
|
||||||
|
+ global $mysqli;
|
||||||
|
+
|
||||||
|
+ $item_id = intval($item_id);
|
||||||
|
+
|
||||||
|
+ mysqli_query($mysqli, "UPDATE shared_items
|
||||||
|
+ SET item_views = item_views + 1
|
||||||
|
+ WHERE item_id = $item_id
|
||||||
|
+ AND item_active = 1
|
||||||
|
+ AND item_expire_at > NOW()
|
||||||
|
+ AND (COALESCE(item_view_limit, 0) = 0 OR item_views < item_view_limit)"
|
||||||
|
+ );
|
||||||
|
+
|
||||||
|
+ // -1 (query error) and 0 (limit reached / revoked / expired) both deny
|
||||||
|
+ return mysqli_affected_rows($mysqli) === 1;
|
||||||
|
+}
|
||||||
|
diff --git a/guest/guest_download_file.php b/guest/guest_download_file.php
|
||||||
|
index 22610f3..c6cd12b 100644
|
||||||
|
--- a/guest/guest_download_file.php
|
||||||
|
+++ b/guest/guest_download_file.php
|
||||||
|
@@ -56,6 +56,12 @@ if (isset($_GET['id']) && isset($_GET['key'])) {
|
||||||
|
exit("Item cannot be viewed at this time (No file, may have been deleted).");
|
||||||
|
}
|
||||||
|
|
||||||
|
+ // Claim the view before the file is served. The checks above stay as a
|
||||||
|
+ // fast path for messaging - this UPDATE is what enforces the limit.
|
||||||
|
+ if (!claimSharedItemView($item_id)) {
|
||||||
|
+ exit("Item cannot be viewed at this time (view limit exceeded).");
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
$file_name = escapeSql($file_row['file_name']);
|
||||||
|
$file_reference_name = escapeSql($file_row['file_reference_name']);
|
||||||
|
$client_id = intval($file_row['file_client_id']);
|
||||||
|
@@ -67,10 +73,6 @@ if (isset($_GET['id']) && isset($_GET['key'])) {
|
||||||
|
header('Content-Disposition: attachment; filename=' . $file_name);
|
||||||
|
readfile($file_path);
|
||||||
|
|
||||||
|
- // Update file view count
|
||||||
|
- $new_item_views = $item_views + 1;
|
||||||
|
- mysqli_query($mysqli, "UPDATE shared_items SET item_views = $new_item_views WHERE item_id = $item_id");
|
||||||
|
-
|
||||||
|
//Logging
|
||||||
|
logAudit("Share", "View", "Downloaded shared file $file_name via link", $client_id);
|
||||||
|
|
||||||
|
diff --git a/guest/guest_post.php b/guest/guest_post.php
|
||||||
|
index 7cec8a9..6ab945d 100644
|
||||||
|
--- a/guest/guest_post.php
|
||||||
|
+++ b/guest/guest_post.php
|
||||||
|
@@ -13,6 +13,10 @@ session_start();
|
||||||
|
|
||||||
|
require_once "../includes/inc_set_timezone.php"; // Must be included after session_start to work
|
||||||
|
|
||||||
|
+// logAudit() reads these globals - without them guest audit rows have no IP
|
||||||
|
+$session_ip = escapeSql(getIP());
|
||||||
|
+$session_user_agent = escapeSql($_SERVER['HTTP_USER_AGENT']);
|
||||||
|
+
|
||||||
|
if (isset($_GET['accept_quote'], $_GET['url_key'])) {
|
||||||
|
|
||||||
|
$quote_id = intval($_GET['accept_quote']);
|
||||||
|
diff --git a/guest/guest_view_item.php b/guest/guest_view_item.php
|
||||||
|
index e6b0417..68f87fc 100644
|
||||||
|
--- a/guest/guest_view_item.php
|
||||||
|
+++ b/guest/guest_view_item.php
|
||||||
|
@@ -128,6 +128,14 @@ if ($item_type == "Document") {
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
+ // Claim the view before any content is disclosed
|
||||||
|
+ if (!claimSharedItemView($item_id)) {
|
||||||
|
+ echo "<div class='alert alert-danger'>Item cannot be viewed at this time. Check with the person that sent you this link to ensure it is correct and has not expired.</div>";
|
||||||
|
+ require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/footer.php';
|
||||||
|
+
|
||||||
|
+ exit();
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
$doc_title = escapeHtml($doc_row['document_name']);
|
||||||
|
$doc_title_escaped = escapeSql($doc_row['document_name']);
|
||||||
|
$doc_content = $purifier->purify($doc_row['document_content']);
|
||||||
|
@@ -135,10 +143,6 @@ if ($item_type == "Document") {
|
||||||
|
echo "<h3>$doc_title</h3>";
|
||||||
|
echo "<div class='prettyContent'>$doc_content</div>";
|
||||||
|
|
||||||
|
- // Update document view count
|
||||||
|
- $new_item_views = $item_views + 1;
|
||||||
|
- mysqli_query($mysqli, "UPDATE shared_items SET item_views = $new_item_views WHERE item_id = $item_id");
|
||||||
|
-
|
||||||
|
// Logging
|
||||||
|
$name = mysqli_real_escape_string($mysqli, $doc_title);
|
||||||
|
logAudit("Share", "View", "Viewed shared $item_type $doc_title_escaped via link", $client_id);
|
||||||
|
@@ -176,6 +180,14 @@ if ($item_type == "Document") {
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
+ // Claim the view before the credential is decrypted or rendered
|
||||||
|
+ if (!claimSharedItemView($item_id)) {
|
||||||
|
+ echo "<div class='alert alert-danger'>Item cannot be viewed at this time. Check with the person that sent you this link to ensure it is correct and has not expired.</div>";
|
||||||
|
+ require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/footer.php';
|
||||||
|
+
|
||||||
|
+ exit();
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
$credential_id = intval($credential_row['credential_id']);
|
||||||
|
$credential_name = escapeHtml($credential_row['credential_name']);
|
||||||
|
$credential_uri = escapeHtml($credential_row['credential_uri']);
|
||||||
|
@@ -254,10 +266,6 @@ if ($item_type == "Document") {
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
- // Update credential view count
|
||||||
|
- $new_item_views = $item_views + 1;
|
||||||
|
- mysqli_query($mysqli, "UPDATE shared_items SET item_views = $new_item_views WHERE item_id = $item_id");
|
||||||
|
-
|
||||||
|
// Logging
|
||||||
|
$name = escapeSql($credential_row['credential_name']);
|
||||||
|
logAudit("Share", "View", "Viewed shared $item_type $name via link", $client_id);
|
||||||
|
diff --git a/guest/includes/inc_all_guest.php b/guest/includes/inc_all_guest.php
|
||||||
|
index 3a4bdce..5bb9505 100644
|
||||||
|
--- a/guest/includes/inc_all_guest.php
|
||||||
|
+++ b/guest/includes/inc_all_guest.php
|
||||||
|
@@ -14,6 +14,10 @@ $user_agent = escapeSql($_SERVER['HTTP_USER_AGENT']);
|
||||||
|
$os = escapeSql(getOS($user_agent));
|
||||||
|
$browser = escapeSql(getWebBrowser($user_agent));
|
||||||
|
|
||||||
|
+// logAudit() reads these globals - without them guest audit rows have no IP
|
||||||
|
+$session_ip = $ip;
|
||||||
|
+$session_user_agent = $user_agent;
|
||||||
|
+
|
||||||
|
// Get Company Name
|
||||||
|
$sql = mysqli_query($mysqli, "SELECT company_name FROM companies WHERE company_id = 1");
|
||||||
|
$row = mysqli_fetch_assoc($sql);
|
||||||
Reference in New Issue
Block a user