Skip to content

ruff

d89dd5e
Select commit
Loading
Failed to load commit list.
Closed

Auditlog: Add django-pghistory as audit log (optional for now) #13126

ruff
d89dd5e
Select commit
Loading
Failed to load commit list.
DryRunSecurity / Cross-Site Scripting Analyzer succeeded Sep 12, 2025 in 1s

DryRun Security

Details

Cross-Site Scripting Analyzer Findings: 1 detected

⚠️ Potential Cross-Site Scripting dojo/templates/dojo/action_history.html (click for details)
Type Potential Cross-Site Scripting
Description File: dojo/templates/dojo/action_history.html -- The patch directly renders user-controllable values into the HTML response in multiple locations that can lead to XSS:

1) <a href="{{ h.url }}" ... title="{{ h.url }}">: The template inserts h.url directly into the href attribute and into the title attribute. Even though Django templates escape HTML special characters, an attacker-controlled value such as a "javascript:alert(1)" URI will remain a valid URI in the href and will execute when clicked. Using user-provided URLs as href targets without validation allows injection of javascript: or other harmful schemes and is a reflected/DOM XSS vector.

2) {{ h.pgh_data
Filename dojo/templates/dojo/action_history.html
CodeLink
{{ block.super }}
<div class="row">
<div class="col-md-12">
{% if pghistory_history %}
<div class="panel panel-default">
<div class="panel-heading tight">
<h4>
PostgreSQL History (pghistory)
<div class="dropdown pull-right">
<button id="show-pghistory-filters" data-toggle="collapse" data-target="#pghistory-filters" class="btn btn-primary toggle-filters" aria-label="Filters"> <i class="fa-solid fa-filter"></i> <i class="caret"></i> </button>
</div>
</h4>
</div>
<div id="pghistory-filters" class="is-filters panel-body collapse {% if pghistory_filter.form.has_changed %}in{% endif %}">
{% include "dojo/filter_snippet.html" with form=pghistory_filter.form %}
</div>
<div class="clearfix">
{% include "dojo/paging_snippet.html" with page=pghistory_history %}
</div>
<div class="table-responsive">
<table class="tablesorter-bootstrap table table-bordered table-condensed table-striped table-hover">
<tr>
<th>Timestamp</th>
<th>Label</th>
<th>Object</th>
<th>User</th>
<th>URL</th>
<th>IP Address</th>
<th>Data</th>
<th>Context</th>
<th>Object ID</th>
<th>Changes</th>
</tr>
{% for h in pghistory_history %}
<tr>
<td>{{ h.pgh_created_at }}</td>
<td>{{ h.pgh_label }}</td>
<td>{{ h.object_str|default:"N/A" }}</td>
<td>{{ h.user|default:"N/A" }}</td>
<td>
{% if h.url and h.url != "N/A" %}
<a href="{{ h.url }}" target="_blank" title="{{ h.url }}">{{ h.url|truncatechars:50 }}</a>
{% else %}
N/A
{% endif %}
</td>
<td>{{ h.remote_addr|default:"N/A" }}</td>
<td>
<details>
<summary style="cursor: pointer; color: #007bff; text-decoration: underline; font-weight: 500;">
<i class="fa fa-plus-circle" style="margin-right: 5px;"></i>View
</summary>
<pre>{{ h.pgh_data|pprint|default:"N/A" }}</pre>
</details>
</td>
<td>
{% if h.pgh_context %}
<details>
<summary style="cursor: pointer; color: #007bff; text-decoration: underline; font-weight: 500;">
<i class="fa fa-plus-circle" style="margin-right: 5px;"></i>View
</summary>
<pre>{{ h.pgh_context|pprint|default:"N/A" }}</pre>
</details>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
<td>{{ h.pgh_obj_id|default:"N/A" }}</td>
<td>
{% if h.pgh_label == "initial_import" %}
<span class="badge badge-info">Initial Import</span>
{% elif h.pgh_diff %}
<div>
{% for field, values in h.pgh_diff.items %}
<div style="margin-bottom: 4px;">
<strong>{{ field }}:</strong>
<span class="text-danger">
{% if values.0 %}
{{ values.0|truncatechars:50 }}
{% else %}
<em>empty</em>
{% endif %}
</span>
to
<span class="text-success">
{% if values.1 %}
{{ values.1|truncatechars:50 }}
{% else %}
<em>empty</em>
{% endif %}
</span>
</div>
{% endfor %}
</div>
{% else %}
<span class="text-muted">No Changes</span>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
</div>
<div class="clearfix">
{% include "dojo/paging_snippet.html" with page=pghistory_history %}
</div>
</div>
{% endif %}
{% if auditlog_history %}
<div class="panel panel-default">
<div class="panel-heading tight">
<h4>
Audit Log History (django-auditlog)
<div class="dropdown pull-right">
<button id="show-filters" data-toggle="collapse" data-target="#the-filters" class="btn btn-primary toggle-filters" aria-label="Filters"> <i class="fa-solid fa-filter"></i> <i class="caret"></i> </button>
</div>
</h4>
</div>
<div id="the-filters" class="is-filters panel-body collapse {% if log_entry_filter.form.has_changed %}in{% endif %}">
{% include "dojo/filter_snippet.html" with form=log_entry_filter.form %}
</div>
<div class="clearfix">
{% include "dojo/paging_snippet.html" with page=auditlog_history %}
</div>
<div class="table-responsive">
<table class="tablesorter-bootstrap table table-bordered table-condensed table-striped table-hover">
<tr>
<th>Action</th>
<th>Actor</th>
<th>Date/Time</th>
<th>Changes</th>
</tr>
{% for h in auditlog_history %}
<tr>
<td>{{ h }}</td>
<td>{{ h.actor }}</td>
<td>{{ h.timestamp }}</td>
<td>
{{ h.changes|action_log_entry|linebreaks}}
</td>
</tr>
{% endfor %}
</table>
</div>
<div class="clearfix">
{% include "dojo/paging_snippet.html" with page=auditlog_history %}
</div>
</div>
{% endif %}
{% if not pghistory_history and not auditlog_history %}
<p class="text-center">No update history found for this object</p>
{% endif %}
</div>
</div>