diff --git a/app/controllers/Reports.php b/app/controllers/Reports.php index 6d493752..99017f24 100644 --- a/app/controllers/Reports.php +++ b/app/controllers/Reports.php @@ -129,6 +129,10 @@ private function renderView($report) foreach ($this->rows as $value) { $this->view->renderData($value, $report[$value]); } + + // Render DOM size information + $this->view->renderCondition('domTooLarge', $report['dom_too_large'] ?? false); + $this->view->renderData('reportId', $report['id']); } /** @@ -296,4 +300,40 @@ private function hasReportPermissions($id) return false; } + + /** + * Downloads DOM content for a report + * + * @param string $id The report id + * @throws Exception + * @return void + */ + public function downloaddom($id) + { + $this->isLoggedInOrExit(); + + // Check report permissions + if (!is_numeric($id) || !$this->hasReportPermissions($id)) { + throw new Exception('You dont have permissions to this report'); + } + + // Get report with DOM data included regardless of size + $report = $this->model('Report')->getById($id, true); + + if (empty($report['dom'])) { + throw new Exception('No DOM content available for this report'); + } + + // Set headers for download with attachment content type to prevent XSS + header('Content-Type: application/octet-stream'); + header('Content-Disposition: attachment; filename="report-' . $id . '-dom.html"'); + header('Content-Length: ' . strlen($report['dom'])); + header('Cache-Control: no-cache, no-store, must-revalidate'); + header('Pragma: no-cache'); + header('Expires: 0'); + + // Output DOM content + echo $report['dom']; + exit; + } } \ No newline at end of file diff --git a/app/controllers/Settings.php b/app/controllers/Settings.php index 96da3a00..267c681d 100644 --- a/app/controllers/Settings.php +++ b/app/controllers/Settings.php @@ -35,7 +35,8 @@ public function index() $logging = _POST('logging'); $storescreenshot = _POST('storescreenshot'); $compress = _POST('compress'); - $this->applicationSettings($timezone, $theme, $filter, $logging, $storescreenshot, $compress); + $domThreshold = _POST('dom_threshold'); + $this->applicationSettings($timezone, $theme, $filter, $logging, $storescreenshot, $compress, $domThreshold); } // Check if posted data is changing global payload settings @@ -106,6 +107,16 @@ public function index() $this->view->renderData('compress0', $settings->get('compress') == 0 ? 'selected' : ''); $this->view->renderData('compress1', $settings->get('compress') == 1 ? 'selected' : ''); + // Render DOM threshold value (convert bytes to MB for display) + try { + $domThresholdBytes = $settings->get('dom_threshold'); + $domThresholdMB = $domThresholdBytes / (1024 * 1024); + } catch (Exception $e) { + // Default to 2MB if setting doesn't exist yet + $domThresholdMB = 2; + } + $this->view->renderData('domThreshold', $domThresholdMB); + $this->view->renderChecked('persistent', $settings->get('persistent') === '1'); $this->view->renderChecked('spider', $settings->get('spider') === '1'); @@ -183,10 +194,11 @@ public function index() * @param string $logging Enable logging * @param string $storescreenshot Method of screenshot storage * @param string $compress Enable compressing + * @param string $domThreshold DOM size threshold in bytes * @throws Exception * @return void */ - private function applicationSettings($timezone, $theme, $filter, $logging, $storescreenshot, $compress) + private function applicationSettings($timezone, $theme, $filter, $logging, $storescreenshot, $compress, $domThreshold) { // Validate timezone if (!in_array($timezone, timezone_identifiers_list(), true)) { @@ -203,6 +215,12 @@ private function applicationSettings($timezone, $theme, $filter, $logging, $stor $filterSave = ($filter == 1 || $filter == 2) ? 1 : 0; $filterAlert = ($filter == 1 || $filter == 3) ? 1 : 0; + // Validate DOM threshold (must be a positive integer, convert MB to bytes) + if (!is_numeric($domThreshold) || $domThreshold < 0) { + throw new Exception('DOM threshold must be a positive number'); + } + $domThresholdBytes = intval($domThreshold * 1024 * 1024); // Convert MB to bytes + // Save settings $this->model('Setting')->set('filter-save', $filterSave); $this->model('Setting')->set('filter-alert', $filterAlert); @@ -211,6 +229,7 @@ private function applicationSettings($timezone, $theme, $filter, $logging, $stor $this->model('Setting')->set('logging', $logging === '1' ? '1' : '0'); $this->model('Setting')->set('storescreenshot', $storescreenshot === '1' ? '1' : '0'); $this->model('Setting')->set('compress', $compress === '1' ? '1' : '0'); + $this->model('Setting')->set('dom_threshold', $domThresholdBytes); $this->log('Updated admin application settings'); } diff --git a/app/models/Report.php b/app/models/Report.php index 6d1f4e2c..f5599b09 100644 --- a/app/models/Report.php +++ b/app/models/Report.php @@ -89,13 +89,14 @@ public function getAllByArchive($archive) * Get report by id * * @param mixed $id The report id + * @param bool $includeLargeDOM Whether to include DOM data regardless of size * @throws Exception * @return array */ - public function getById($id) + public function getById($id, $includeLargeDOM = false) { $report = parent::getById($id); - $report_data = $this->getReportData($report['id']); + $report_data = $this->getReportData($report['id'], $includeLargeDOM); return array_merge($report, $report_data); } @@ -104,10 +105,11 @@ public function getById($id) * Get report by share id * * @param mixed $id The share id + * @param bool $includeLargeDOM Whether to include DOM data regardless of size * @throws Exception * @return array */ - public function getByShareId($id) + public function getByShareId($id, $includeLargeDOM = false) { $database = Database::openConnection(); $database->prepare("SELECT * FROM $this->table WHERE `shareid` = :shareid LIMIT 1"); @@ -119,7 +121,7 @@ public function getByShareId($id) } $report = $database->fetch(); - $report_data = $this->getReportData($report['id']); + $report_data = $this->getReportData($report['id'], $includeLargeDOM); return array_merge($report, $report_data); } @@ -394,12 +396,42 @@ public function getAllInvalid() * Get big data from report by id * * @param int $id The report id + * @param bool $includeLargeDOM Whether to include DOM data regardless of size * @return array */ - private function getReportData($id) + private function getReportData($id, $includeLargeDOM = false) { $database = Database::openConnection(); - $database->prepare("SELECT `dom`,`screenshot`,`localstorage`,`sessionstorage`,`extra`,`compressed` FROM $this->table_data WHERE `reportid` = :reportid LIMIT 1"); + + // First, check DOM size if we need to conditionally exclude it + $domThreshold = $this->getDomThreshold(); + $selectColumns = "`screenshot`,`localstorage`,`sessionstorage`,`extra`,`compressed`"; + $shouldIncludeDOM = $includeLargeDOM; + + if (!$includeLargeDOM) { + // Check DOM size first + $database->prepare("SELECT LENGTH(`dom`) as dom_size, `compressed` FROM $this->table_data WHERE `reportid` = :reportid LIMIT 1"); + $database->bindValue(':reportid', $id); + $database->execute(); + + if ($database->countRows() === 1) { + $sizeData = $database->fetch(); + $domSize = $sizeData['dom_size']; + + // If compressed, estimate uncompressed size (rough estimate: 3x compressed size) + if ($sizeData['compressed'] == 1) { + $domSize = $domSize * 3; + } + + $shouldIncludeDOM = $domSize <= $domThreshold; + } + } + + if ($shouldIncludeDOM) { + $selectColumns = "`dom`," . $selectColumns; + } + + $database->prepare("SELECT $selectColumns FROM $this->table_data WHERE `reportid` = :reportid LIMIT 1"); $database->bindValue(':reportid', $id); $database->execute(); @@ -407,15 +439,41 @@ private function getReportData($id) $report_data = $database->fetch(); } + // Initialize DOM field if not included + if (!$shouldIncludeDOM) { + $report_data['dom'] = ''; + $report_data['dom_too_large'] = true; + } else { + $report_data['dom_too_large'] = false; + } + // Decompress if compressed if($report_data['compressed'] ?? 0 == 1) { - $report_data['dom'] = !empty($report_data['dom']) ? gzinflate(base64_decode($report_data['dom'])) : ''; + if ($shouldIncludeDOM && !empty($report_data['dom'])) { + $report_data['dom'] = gzinflate(base64_decode($report_data['dom'])); + } $report_data['screenshot'] = strlen($report_data['screenshot']) === 52 || empty($report_data['screenshot']) ? $report_data['screenshot'] : base64_encode(gzinflate(base64_decode($report_data['screenshot']))) ?? ''; $report_data['localstorage'] = $report_data['localstorage'] === '{}' ? '{}' : gzinflate(base64_decode($report_data['localstorage'])) ?? ''; $report_data['sessionstorage'] = $report_data['sessionstorage'] === '{}' ? '{}' : gzinflate(base64_decode($report_data['sessionstorage'])) ?? ''; $report_data['extra'] = !empty($report_data['extra']) ? gzinflate(base64_decode($report_data['extra'])) : ''; } - return $report_data ?? ['dom' => '', 'screenshot' => '', 'localstorage' => '', 'sessionstorage' => '', 'extra' => '']; + return $report_data ?? ['dom' => '', 'screenshot' => '', 'localstorage' => '', 'sessionstorage' => '', 'extra' => '', 'dom_too_large' => false]; + } + + /** + * Get DOM threshold setting in bytes + * + * @return int + */ + private function getDomThreshold() + { + try { + $settingModel = new Setting_model(); + return intval($settingModel->get('dom_threshold')); + } catch (Exception $e) { + // Default to 2MB if setting doesn't exist + return 2097152; // 2 * 1024 * 1024 bytes + } } } diff --git a/app/sql/4.3-4.4.sql b/app/sql/4.3-4.4.sql new file mode 100644 index 00000000..f3b1c3b3 --- /dev/null +++ b/app/sql/4.3-4.4.sql @@ -0,0 +1,7 @@ +-- Migration from ezXSS 4.3 to 4.4 +-- Adds DOM threshold setting + +INSERT INTO `settings` (`setting`, `value`) VALUES ('dom_threshold', '2097152') +ON DUPLICATE KEY UPDATE `value` = VALUES(`value`); + +-- Note: 2097152 bytes = 2MB default threshold diff --git a/app/views/reports/view.html b/app/views/reports/view.html index 75e0ac2b..7ad94571 100644 --- a/app/views/reports/view.html +++ b/app/views/reports/view.html @@ -128,6 +128,19 @@

View report

DOM + {%if domTooLarge} +
+
+ DOM content is too large to display safely. +
+ Use the download button below to save it as a file. +
+ + Download DOM Content + +
+ {%/if} + {%!if domTooLarge}
@@ -135,6 +148,7 @@

View report

Copy
+ {%/!if} diff --git a/app/views/settings/index.html b/app/views/settings/index.html index 143aecc4..1b0af37f 100644 --- a/app/views/settings/index.html +++ b/app/views/settings/index.html @@ -73,6 +73,13 @@

Settings

+
+ + + When DOM size exceeds this threshold, show download button instead of textarea +
@@ -431,4 +438,4 @@

Get Telegram Chat ID

- \ No newline at end of file +