Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions app/controllers/Reports.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
}

/**
Expand Down Expand Up @@ -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;
}
}
23 changes: 21 additions & 2 deletions app/controllers/Settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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');

Expand Down Expand Up @@ -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)) {
Expand All @@ -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);
Expand All @@ -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');
}

Expand Down
74 changes: 66 additions & 8 deletions app/models/Report.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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");
Expand All @@ -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);
}
Expand Down Expand Up @@ -394,28 +396,84 @@ 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();

if ($database->countRows() === 1) {
$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
}
}
}
7 changes: 7 additions & 0 deletions app/sql/4.3-4.4.sql
Original file line number Diff line number Diff line change
@@ -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
14 changes: 14 additions & 0 deletions app/views/reports/view.html
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,27 @@ <h3 class="m-b-xs">View report</h3>
<strong>DOM</strong>
</td>
<td class="field-content">
{%if domTooLarge}
<div class="data-container">
<div class="alert alert-info">
<strong>DOM content is too large to display safely.</strong>
<br>
<small>Use the download button below to save it as a file.</small>
</div>
<a href="/manage/reports/downloaddom/{%data reportId}" class="btn btn-primary">
<i class="fa fa-download"></i> Download DOM Content
</a>
</div>
{%/if}
{%!if domTooLarge}
<div class="data-container">
<textarea spellcheck="false" class="form-control modern-textarea dom-textarea"
rows="12" id="dom" readonly>{%data dom}</textarea>
<button class="btn btn-sm btn-outline-secondary copy-btn" data-target="#dom">
Copy
</button>
</div>
{%/!if}
</td>
</tr>
<tr class="report-row">
Expand Down
9 changes: 8 additions & 1 deletion app/views/settings/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ <h3 class="m-b-xs">Settings</h3>
<option {%data compress1} value="1">Compressed and encoded</option>
</select>
</div>
<div class="form-group">
<label class="control-label" for="dom_threshold">DOM size threshold (MB)</label>
<input type="number" name="dom_threshold" id="dom_threshold" class="form-control"
value="{%data domThreshold}" min="0" step="0.1"
placeholder="2">
<small>When DOM size exceeds this threshold, show download button instead of textarea</small>
</div>
<button name="application" type="submit" class="btn">Save</button>
</form>
</div>
Expand Down Expand Up @@ -431,4 +438,4 @@ <h3 class="m-b-xs">Get Telegram Chat ID</h3>
</div>
</div>
</div>
</div>
</div>