diff --git a/ad_miner/scripts/analyse_cache.py b/ad_miner/scripts/analyse_cache.py
index ab6cc29b..0532eada 100644
--- a/ad_miner/scripts/analyse_cache.py
+++ b/ad_miner/scripts/analyse_cache.py
@@ -1,19 +1,34 @@
-import pickle
+import os
import sys
from pathlib import Path
+from ad_miner.sources.modules.safe_pickle import safe_load
+
# Constants
MODULES_DIRECTORY = Path(__file__).parent / 'sources/modules'
def request_a():
module_name = sys.argv[1]
- return retrieveCacheEntry(full_module_path=MODULES_DIRECTORY / module_name)
+ # Security: Validate that module_name is a simple filename without path components
+ if os.path.sep in module_name or '/' in module_name or module_name.startswith('.'):
+ raise ValueError(f"Invalid module name: path components not allowed")
+
+ return retrieveCacheEntry(module_name=module_name)
+
+
+def retrieveCacheEntry(module_name: str):
+ # Resolve the full path
+ full_path = (MODULES_DIRECTORY / module_name).resolve()
+
+ # Security: Verify the path stays within the allowed directory
+ modules_dir_resolved = MODULES_DIRECTORY.resolve()
+ if not str(full_path).startswith(str(modules_dir_resolved)):
+ raise ValueError("Path traversal detected: access denied")
-def retrieveCacheEntry(full_module_path: Path):
- with open(full_module_path, "rb") as f:
- return pickle.load(f)
+ with open(full_path, "rb") as f:
+ return safe_load(f)
list_path = request_a()
diff --git a/ad_miner/sources/js/graph.js b/ad_miner/sources/js/graph.js
index 43fbed08..a95b3ba1 100644
--- a/ad_miner/sources/js/graph.js
+++ b/ad_miner/sources/js/graph.js
@@ -1,6 +1,13 @@
// This object is used by vis.js to retrieve node icon
var icon_group_options = {};
+// HTML escape function to prevent XSS
+function escapeHtmlGraph(text) {
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+}
+
var existing_attribute_strings = []
for (var i = 0; i < window.data_nodes.length; i++) {
@@ -1755,6 +1762,8 @@ function bindRightClick() {
No Action
`;
} else {
+ // Sanitize lastSelectedNode by using JSON.stringify to prevent injection
+ var safeNodeId = JSON.stringify(lastSelectedNode);
if (
allNodescopy[lastSelectedNode].clusterType == 'complete' ||
allNodescopy[lastSelectedNode].clusterType == 'partial' ||
@@ -1762,22 +1771,14 @@ function bindRightClick() {
) {
menu.innerHTML =
`
diff --git a/ad_miner/sources/modules/cache_class.py b/ad_miner/sources/modules/cache_class.py
index 037f34ae..eaada7dc 100755
--- a/ad_miner/sources/modules/cache_class.py
+++ b/ad_miner/sources/modules/cache_class.py
@@ -2,6 +2,8 @@
import pickle
import csv
+from ad_miner.sources.modules.safe_pickle import safe_load
+
class Cache:
def __init__(self, arguments):
@@ -25,7 +27,7 @@ def retrieveCacheEntry(self, filename):
full_name = self.cache_prefix + "_" + filename
if os.path.exists(full_name):
with open(full_name, "rb") as f:
- return pickle.load(f)
+ return safe_load(f)
return False
def createCsvFileFromRequest(self, filename, data, object_type):
diff --git a/ad_miner/sources/modules/common_analysis.py b/ad_miner/sources/modules/common_analysis.py
index ed77d733..8468bb07 100644
--- a/ad_miner/sources/modules/common_analysis.py
+++ b/ad_miner/sources/modules/common_analysis.py
@@ -1,5 +1,5 @@
from ad_miner.sources.modules import logger
-from ad_miner.sources.modules.utils import CONFIG_MAP, days_format
+from ad_miner.sources.modules.utils import CONFIG_MAP, days_format, escape_html
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.graph_class import Graph
from ad_miner.sources.modules.path_neo4j import Path
@@ -517,16 +517,16 @@ def genNumberOfDCPage(requests_results, arguments):
for d in computers_nb_domain_controllers:
temp_data = {}
- temp_data["domain"] = '
' + d["domain"]
+ temp_data["domain"] = '
' + escape_html(d["domain"])
if d["ghost"]:
temp_data["name"] = (
'
'
- + d["name"]
+ + escape_html(d["name"])
)
else:
- temp_data["name"] = '
' + d["name"]
+ temp_data["name"] = '
' + escape_html(d["name"])
if "WINDOWS" in d["os"].upper():
- temp_data["os"] = '
' + d["os"]
+ temp_data["os"] = '
' + escape_html(d["os"])
temp_data["last logon"] = days_format(d["lastLogon"])
data.append(temp_data)
grid.setData(data)
@@ -561,15 +561,15 @@ def genUsersListPage(requests_results, arguments):
data = []
for user in users:
tmp_dict = {}
- tmp_dict["domain"] = '
' + user["domain"]
+ tmp_dict["domain"] = '
' + escape_html(user["domain"])
# Add admin icon
if user["name"] in admin_list:
tmp_dict["name"] = (
'
'
- + user["name"]
+ + escape_html(user["name"])
)
else:
- tmp_dict["name"] = '
' + user["name"]
+ tmp_dict["name"] = '
' + escape_html(user["name"])
# Add calendar icon
logon = -1
if user.get("logon"):
@@ -601,12 +601,12 @@ def genAllGroupsPage(requests_results, arguments):
grid.setheaders(["domain", "name"])
group_extract = [
{
- "domain": '
' + groups[k]["domain"],
+ "domain": '
' + escape_html(groups[k]["domain"]),
"name": (
'
'
- + groups[k]["name"]
+ + escape_html(groups[k]["name"])
if groups[k].get("da")
- else '
' + groups[k]["name"]
+ else '
' + escape_html(groups[k]["name"])
),
}
for k in range(len(groups))
@@ -641,13 +641,13 @@ def generateComputersListPage(requests_results, arguments):
if computer["ghost"]:
name = (
'
'
- + computer["name"]
+ + escape_html(computer["name"])
)
else:
- name = '
' + computer["name"]
+ name = '
' + escape_html(computer["name"])
# OS
if computer["os"]:
- os = computer["os"]
+ os = escape_html(computer["os"])
if "windows" in computer["os"].lower():
os = '
' + os
elif "mac" in computer["os"].lower():
@@ -657,7 +657,7 @@ def generateComputersListPage(requests_results, arguments):
else:
os = "Unknown"
formated_computer = {
- "domain": '
' + computer["domain"],
+ "domain": '
' + escape_html(computer["domain"]),
"name": name,
"operating system": os,
}
@@ -689,8 +689,8 @@ def generateADCSListPage(requests_results, arguments):
grid = Grid("ADCS servers")
grid.setheaders(["domain", "name"])
for adcs in computers_adcs:
- adcs["domain"] = '
' + adcs["domain"]
- adcs["name"] = '
' + adcs["name"]
+ adcs["domain"] = '
' + escape_html(adcs["domain"])
+ adcs["name"] = '
' + escape_html(adcs["name"])
grid.setData(computers_adcs)
page.addComponent(grid)
page.render()
@@ -734,8 +734,8 @@ def genAzureTenants(requests_results, arguments):
data.append(
{
"Tenant ID": '
'
- + tenant["ID"],
- "Tenant Name": '
' + tenant["Name"],
+ + escape_html(tenant["ID"]),
+ "Tenant Name": '
' + escape_html(tenant["Name"]),
}
)
@@ -774,15 +774,15 @@ def genAzureUsers(requests_results, arguments):
tenant_name = tenant_id_name.get(tenant_id, tenant_id)
data.append(
{
- "Tenant Name": '
' + tenant_name,
- "Name": '
' + user["Name"],
+ "Tenant Name": '
' + escape_html(tenant_name),
+ "Name": '
' + escape_html(user["Name"]),
"Synced on premise": (
'
'
if user["onpremisesynced"] == True
else '
'
),
"On premise SID": (
- user["SID"]
+ escape_html(user["SID"])
if user["onpremisesynced"] == True and user["SID"] != None
else "-"
),
@@ -824,8 +824,8 @@ def genAzureAdmin(requests_results, arguments):
tenant_name = tenant_id_name.get(tenant_id, tenant_id)
data.append(
{
- "Tenant Name": '
' + tenant_name,
- "Name": '
' + admin["Name"],
+ "Tenant Name": '
' + escape_html(tenant_name),
+ "Name": '
' + escape_html(admin["Name"]),
}
)
@@ -864,9 +864,9 @@ def genAzureGroups(requests_results, arguments):
tenant_name = tenant_id_name.get(tenant_id, tenant_id)
data.append(
{
- "Tenant Name": '
' + tenant_name,
- "Name": '
' + group["Name"],
- "Description": group["Description"],
+ "Tenant Name": '
' + escape_html(tenant_name),
+ "Name": '
' + escape_html(group["Name"]),
+ "Description": escape_html(group["Description"]),
}
)
@@ -905,13 +905,13 @@ def genAzureVM(requests_results, arguments):
for dict in azure_vm:
tenant_id = dict.get("Tenant ID")
tenant_name = tenant_id_name.get(tenant_id, tenant_id)
- tmp_data = {"Tenant Name": tenant_name}
+ tmp_data = {"Tenant Name": escape_html(tenant_name)}
- tmp_data["Name"] = dict["Name"]
+ tmp_data["Name"] = escape_html(dict["Name"])
# os
if dict.get("os"):
- os = dict["os"]
+ os = escape_html(dict["os"])
if "windows" in dict["os"].lower():
os = '
' + os
elif "mac" in dict["os"].lower():
@@ -958,13 +958,13 @@ def genAzureDevices(requests_results, arguments):
tenant_id = dict.get("Tenant ID")
tenant_name = tenant_id_name.get(tenant_id, tenant_id)
- tmp_data = {"Tenant Name": tenant_name}
+ tmp_data = {"Tenant Name": escape_html(tenant_name)}
- tmp_data["Name"] = dict["Name"]
+ tmp_data["Name"] = escape_html(dict["Name"])
# os
if dict.get("os"):
- os = dict["os"]
+ os = escape_html(dict["os"])
if "windows" in dict["os"].lower():
os = '
' + os
elif (
@@ -1023,15 +1023,15 @@ def genAzureApps(requests_results, arguments):
if tenant_id == "F8CDEF31-A31E-4B4A-93E4-5F571E91255A":
data_microsft.append(
{
- "Tenant ID": '
' + tenant_name,
- "Name": ' ' + app["Name"],
+ "Tenant ID": '
' + escape_html(tenant_name),
+ "Name": ' ' + escape_html(app["Name"]),
}
)
else:
data.append(
{
- "Tenant ID": '
' + tenant_name,
- "Name": ' ' + app["Name"],
+ "Tenant ID": '
' + escape_html(tenant_name),
+ "Name": ' ' + escape_html(app["Name"]),
}
)
data += data_microsft
diff --git a/ad_miner/sources/modules/controls/anomaly_acl.py b/ad_miner/sources/modules/controls/anomaly_acl.py
index 66b1334a..f74ad508 100644
--- a/ad_miner/sources/modules/controls/anomaly_acl.py
+++ b/ad_miner/sources/modules/controls/anomaly_acl.py
@@ -4,7 +4,7 @@
from ad_miner.sources.modules.grid_class import Grid
from ad_miner.sources.modules import generic_formating, generic_computing
-from ad_miner.sources.modules.utils import grid_data_stringify
+from ad_miner.sources.modules.utils import grid_data_stringify, escape_html
from ad_miner.sources.modules.common_analysis import (
get_dico_admin_of_computer_id,
createGraphPage,
@@ -178,7 +178,7 @@ def run(self):
else:
icon = "bi-person-fill"
tmp_dict["targets"] = (
- '
' + name
+ '
' + escape_html(name)
)
if name in self.dico_is_user_admin_on_computer:
count = len(self.users_admin_computer_list[name])
@@ -281,7 +281,7 @@ def run(self):
self.requests_results,
)
- tmp_dict["targets"] = f'
{name}'
+ tmp_dict["targets"] = f'
' + escape_html(name)
formated_data_details.append(tmp_dict)
page = Page(
diff --git a/ad_miner/sources/modules/controls/azure_aadconnect_users.py b/ad_miner/sources/modules/controls/azure_aadconnect_users.py
index 37bc0c34..79ad3e52 100644
--- a/ad_miner/sources/modules/controls/azure_aadconnect_users.py
+++ b/ad_miner/sources/modules/controls/azure_aadconnect_users.py
@@ -3,6 +3,7 @@
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.grid_class import Grid
from ad_miner.sources.modules.common_analysis import presence_of
+from ad_miner.sources.modules.utils import escape_html
@register_control
@@ -40,8 +41,8 @@ def run(self):
data.append(
{
"Tenant ID": '
'
- + f'{user["Tenant ID"] if user["Tenant ID"] != None else "-"}',
- "Name": '
' + user["Name"],
+ + escape_html(f'{user["Tenant ID"] if user["Tenant ID"] != None else "-"}'),
+ "Name": '
' + escape_html(user["Name"]),
"Session": user["Session"] if user["Session"] != None else "-",
}
)
diff --git a/ad_miner/sources/modules/controls/azure_accounts_not_found_on_prem.py b/ad_miner/sources/modules/controls/azure_accounts_not_found_on_prem.py
index 35d869c1..d9716adb 100644
--- a/ad_miner/sources/modules/controls/azure_accounts_not_found_on_prem.py
+++ b/ad_miner/sources/modules/controls/azure_accounts_not_found_on_prem.py
@@ -4,6 +4,7 @@
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.grid_class import Grid
from ad_miner.sources.modules.common_analysis import presence_of
+from ad_miner.sources.modules.utils import escape_html
@register_control
@@ -46,7 +47,7 @@ def run(self):
for user in self.azure_accounts_not_found_on_prem:
data.append(
{
- "Name": '
' + user["Name"],
+ "Name": '
' + escape_html(user["Name"]),
"Synced to on premise": '
',
"Synced account": '
',
}
diff --git a/ad_miner/sources/modules/controls/azure_admin_on_prem.py b/ad_miner/sources/modules/controls/azure_admin_on_prem.py
index f5deaf2f..96e338f3 100644
--- a/ad_miner/sources/modules/controls/azure_admin_on_prem.py
+++ b/ad_miner/sources/modules/controls/azure_admin_on_prem.py
@@ -4,6 +4,7 @@
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.grid_class import Grid
from ad_miner.sources.modules.common_analysis import presence_of
+from ad_miner.sources.modules.utils import escape_html
@register_control
@@ -40,7 +41,7 @@ def run(self):
data = []
for user in self.azure_admin_on_prem:
- data.append({"Name": '
' + user["Name"]})
+ data.append({"Name": '
' + escape_html(user["Name"])})
grid.setheaders(["Name"])
diff --git a/ad_miner/sources/modules/controls/azure_dormant_accounts.py b/ad_miner/sources/modules/controls/azure_dormant_accounts.py
index 8cbda5a2..e612e36b 100644
--- a/ad_miner/sources/modules/controls/azure_dormant_accounts.py
+++ b/ad_miner/sources/modules/controls/azure_dormant_accounts.py
@@ -4,7 +4,7 @@
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.grid_class import Grid
-from ad_miner.sources.modules.utils import days_format
+from ad_miner.sources.modules.utils import days_format, escape_html
from ad_miner.sources.modules.common_analysis import presence_of
@@ -47,7 +47,7 @@ def run(self):
self.azure_dormant_accounts_90_days.append(user)
data.append(
{
- "Name": '
' + user["Name"],
+ "Name": '
' + escape_html(user["Name"]),
"Last logon": days_format(user["lastlogon"]),
"Creation date": days_format(user["whencreated"]),
}
diff --git a/ad_miner/sources/modules/controls/azure_last_passwd_change.py b/ad_miner/sources/modules/controls/azure_last_passwd_change.py
index e3de3737..afa44b23 100644
--- a/ad_miner/sources/modules/controls/azure_last_passwd_change.py
+++ b/ad_miner/sources/modules/controls/azure_last_passwd_change.py
@@ -3,7 +3,7 @@
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.grid_class import Grid
-from ad_miner.sources.modules.utils import days_format
+from ad_miner.sources.modules.utils import days_format, escape_html
from ad_miner.sources.modules.common_analysis import presence_of
@@ -49,7 +49,7 @@ def run(self):
self.azure_last_passwd_change_strange.append(user)
data.append(
{
- "Name": '
' + user["Name"],
+ "Name": '
' + escape_html(user["Name"]),
"Last password set on premise": days_format(onprem),
"Last password set on Azure": days_format(onazure),
"Difference": days_format(diff),
diff --git a/ad_miner/sources/modules/controls/azure_reset_passwd.py b/ad_miner/sources/modules/controls/azure_reset_passwd.py
index 0c6d923e..34d07725 100644
--- a/ad_miner/sources/modules/controls/azure_reset_passwd.py
+++ b/ad_miner/sources/modules/controls/azure_reset_passwd.py
@@ -3,7 +3,7 @@
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.grid_class import Grid
-from ad_miner.sources.modules.utils import grid_data_stringify
+from ad_miner.sources.modules.utils import grid_data_stringify, escape_html
from ad_miner.sources.modules.common_analysis import presence_of
from hashlib import md5
@@ -65,7 +65,7 @@ def run(self):
data.append(
{
- "Privileged user": '
' + user,
+ "Privileged user": '
' + escape_html(user),
"Passwords that can be reset": grid_data_stringify(
{
"link": f"passwords_reset_{hash}.html",
diff --git a/ad_miner/sources/modules/controls/can_dcsync.py b/ad_miner/sources/modules/controls/can_dcsync.py
index 4783fe08..d7c2f6e5 100644
--- a/ad_miner/sources/modules/controls/can_dcsync.py
+++ b/ad_miner/sources/modules/controls/can_dcsync.py
@@ -4,7 +4,7 @@
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.grid_class import Grid
from ad_miner.sources.modules.graph_class import Graph
-from ad_miner.sources.modules.utils import grid_data_stringify
+from ad_miner.sources.modules.utils import grid_data_stringify, escape_html
from ad_miner.sources.modules.common_analysis import presence_of
from urllib.parse import quote
@@ -117,9 +117,9 @@ def genFullDCSync(self):
sortClass = str(len(paths_left)).zfill(6)
data.append(
{
- "domain": '
' + n.domain,
- "type": type_icon + " " + n.labels,
- "name": name_icon + " " + n.name,
+ "domain": '
' + escape_html(n.domain),
+ "type": type_icon + " " + escape_html(n.labels),
+ "name": name_icon + " " + escape_html(n.name),
"path to account": grid_data_stringify(
{
"link": "path_to_%s_with_dcsync.html" % quote(str(n.name)),
diff --git a/ad_miner/sources/modules/controls/can_read_gmsapassword_of_adm.py b/ad_miner/sources/modules/controls/can_read_gmsapassword_of_adm.py
index 4d4098c0..910bec63 100644
--- a/ad_miner/sources/modules/controls/can_read_gmsapassword_of_adm.py
+++ b/ad_miner/sources/modules/controls/can_read_gmsapassword_of_adm.py
@@ -4,7 +4,7 @@
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.grid_class import Grid
from ad_miner.sources.modules.graph_class import Graph
-from ad_miner.sources.modules.utils import grid_data_stringify
+from ad_miner.sources.modules.utils import grid_data_stringify, escape_html
from ad_miner.sources.modules.common_analysis import presence_of
from urllib.parse import quote
@@ -72,8 +72,8 @@ def run(self):
for d in data.values():
sortClass = str(len(d["paths"])).zfill(6)
tmp_grid_data = {
- "domain": '
' + d["domain"],
- "name": '
' + d["name"],
+ "domain": '
' + escape_html(d["domain"]),
+ "name": '
' + escape_html(d["name"]),
"target": grid_data_stringify(
{
"value": f"{len(d['paths'])} paths to {len(d['target'])} target{'s' if len(d['target'])>1 else ''}",
diff --git a/ad_miner/sources/modules/controls/can_read_laps.py b/ad_miner/sources/modules/controls/can_read_laps.py
index 4f1b874c..c855657f 100644
--- a/ad_miner/sources/modules/controls/can_read_laps.py
+++ b/ad_miner/sources/modules/controls/can_read_laps.py
@@ -3,6 +3,7 @@
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.grid_class import Grid
+from ad_miner.sources.modules.utils import escape_html
@register_control
@@ -39,8 +40,8 @@ def run(self):
grid.setheaders(["domain", "name"])
self.can_read_laps_parsed = [
{
- "domain": '
' + user["domain"],
- "name": '
' + user["name"],
+ "domain": '
' + escape_html(user["domain"]),
+ "name": '
' + escape_html(user["name"]),
}
for user in self.can_read_laps
if user["domain"] is not None and user["name"] is not None
diff --git a/ad_miner/sources/modules/controls/computers_admin_of_computers.py b/ad_miner/sources/modules/controls/computers_admin_of_computers.py
index ebabc33e..9dbdd3c7 100644
--- a/ad_miner/sources/modules/controls/computers_admin_of_computers.py
+++ b/ad_miner/sources/modules/controls/computers_admin_of_computers.py
@@ -3,7 +3,7 @@
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.grid_class import Grid
-from ad_miner.sources.modules.utils import grid_data_stringify
+from ad_miner.sources.modules.utils import grid_data_stringify, escape_html
from ad_miner.sources.modules import generic_formating, generic_computing
from ad_miner.sources.modules.common_analysis import (
findAndCreatePathToDaFromComputersList,
@@ -67,7 +67,7 @@ def run(self):
tmp_line = {
"Computer Admin": '
'
- + admin_computer,
+ + escape_html(admin_computer),
"Computers count": grid_data_stringify(
{
"value": f"{computers_admin_to_count[admin_computer]} computers",
diff --git a/ad_miner/sources/modules/controls/computers_last_connexion.py b/ad_miner/sources/modules/controls/computers_last_connexion.py
index d1a8cb22..a4b20a7d 100644
--- a/ad_miner/sources/modules/controls/computers_last_connexion.py
+++ b/ad_miner/sources/modules/controls/computers_last_connexion.py
@@ -3,7 +3,7 @@
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.grid_class import Grid
-from ad_miner.sources.modules.utils import days_format
+from ad_miner.sources.modules.utils import days_format, escape_html
from ad_miner.sources.modules.common_analysis import percentage_superior
@@ -49,7 +49,7 @@ def run(self):
for c in self.computers_not_connected_since_60:
data.append(
{
- "name": '
' + c["name"],
+ "name": '
' + escape_html(c["name"]),
"Last logon": days_format(c["days"]),
"Last password set": days_format(c["pwdlastset"]),
"Enabled": str(c["enabled"]),
diff --git a/ad_miner/sources/modules/controls/computers_list_of_rdp_users.py b/ad_miner/sources/modules/controls/computers_list_of_rdp_users.py
index 6cca35fa..607e5a27 100644
--- a/ad_miner/sources/modules/controls/computers_list_of_rdp_users.py
+++ b/ad_miner/sources/modules/controls/computers_list_of_rdp_users.py
@@ -3,7 +3,7 @@
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.grid_class import Grid
-from ad_miner.sources.modules.utils import grid_data_stringify
+from ad_miner.sources.modules.utils import grid_data_stringify, escape_html
from ad_miner.sources.modules.common_analysis import percentage_superior
from urllib.parse import quote
@@ -57,7 +57,7 @@ def run(self):
for key in self.users_rdp_access_2:
sortClass = str(len(self.users_rdp_access_2[key])).zfill(6)
d = {
- "Computers": '
' + key,
+ "Computers": '
' + escape_html(key),
"Users": grid_data_stringify(
{
"value": f"{len(self.users_rdp_access_2[key])} Users
{self.users_rdp_access_2[key]}
",
diff --git a/ad_miner/sources/modules/controls/computers_members_high_privilege.py b/ad_miner/sources/modules/controls/computers_members_high_privilege.py
index 403896eb..d6aa6611 100644
--- a/ad_miner/sources/modules/controls/computers_members_high_privilege.py
+++ b/ad_miner/sources/modules/controls/computers_members_high_privilege.py
@@ -4,6 +4,7 @@
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.grid_class import Grid
from ad_miner.sources.modules.common_analysis import presence_of
+from ad_miner.sources.modules.utils import escape_html
@register_control
@@ -45,9 +46,9 @@ def run(self):
)
grid = Grid("List of computer admins")
for d in self.computers_members_high_privilege:
- d["domain"] = '
' + d["domain"]
- d["computer"] = '
' + d["computer"]
- d["group"] = '
' + d["group"]
+ d["domain"] = '
' + escape_html(d["domain"])
+ d["computer"] = '
' + escape_html(d["computer"])
+ d["group"] = '
' + escape_html(d["group"])
grid.setheaders(["domain", "computer", "group"])
grid.setData(self.computers_members_high_privilege)
page.addComponent(grid)
diff --git a/ad_miner/sources/modules/controls/computers_os_obsolete.py b/ad_miner/sources/modules/controls/computers_os_obsolete.py
index 31083034..864bd6bb 100644
--- a/ad_miner/sources/modules/controls/computers_os_obsolete.py
+++ b/ad_miner/sources/modules/controls/computers_os_obsolete.py
@@ -3,7 +3,7 @@
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.grid_class import Grid
-from ad_miner.sources.modules.utils import days_format
+from ad_miner.sources.modules.utils import days_format, escape_html
from ad_miner.sources.modules.common_analysis import manageComputersOs, presence_of
@@ -41,7 +41,7 @@ def run(self):
for computer in self.list_computers_os_obsolete:
if computer["Last logon in days"] < 90: # remove ghost computers
computer["Domain"] = (
- '
' + computer["Domain"]
+ '
' + escape_html(computer["Domain"])
)
computer["Last logon"] = days_format(computer["Last logon in days"])
if (
@@ -50,10 +50,10 @@ def run(self):
or "2012" in computer["Operating system"]
): # Add icons whether it's a computer or a server
computer["Operating system"] = (
- '
' + computer["Operating system"]
+ '
' + escape_html(computer["Operating system"])
)
computer["name"] = (
- '
' + computer["name"]
+ '
' + escape_html(computer["name"])
)
if (
"2000" in computer["Operating system"]
@@ -62,10 +62,10 @@ def run(self):
):
computer["Operating system"] = (
'
'
- + computer["Operating system"]
+ + escape_html(computer["Operating system"])
)
computer["name"] = (
- '
' + computer["name"]
+ '
' + escape_html(computer["name"])
)
cleaned_data.append(computer)
diff --git a/ad_miner/sources/modules/controls/computers_without_laps.py b/ad_miner/sources/modules/controls/computers_without_laps.py
index 9f59bb98..bb50c112 100644
--- a/ad_miner/sources/modules/controls/computers_without_laps.py
+++ b/ad_miner/sources/modules/controls/computers_without_laps.py
@@ -4,7 +4,7 @@
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.grid_class import Grid
-from ad_miner.sources.modules.utils import days_format
+from ad_miner.sources.modules.utils import days_format, escape_html
@register_control
@@ -66,11 +66,11 @@ def run(self):
# Exclude ghost computers (last logon > 90 days)
if computer["lastLogon"] < 90:
tmp_dict["domain"] = (
- '
' + computer["domain"]
+ '
' + escape_html(computer["domain"])
)
tmp_dict["Last logon"] = days_format(computer["lastLogon"])
tmp_dict["name"] = (
- '
' + computer["name"]
+ '
' + escape_html(computer["name"])
)
if computer["LAPS"] == "false":
tmp_dict["LAPS"] = (
diff --git a/ad_miner/sources/modules/controls/cross_domain_admin_privileges.py b/ad_miner/sources/modules/controls/cross_domain_admin_privileges.py
index 42d71e55..0ec71e82 100644
--- a/ad_miner/sources/modules/controls/cross_domain_admin_privileges.py
+++ b/ad_miner/sources/modules/controls/cross_domain_admin_privileges.py
@@ -2,7 +2,7 @@
from ad_miner.sources.modules.controls import register_control
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.grid_class import Grid
-from ad_miner.sources.modules.utils import grid_data_stringify
+from ad_miner.sources.modules.utils import grid_data_stringify, escape_html
from ad_miner.sources.modules.common_analysis import createGraphPage
import copy
@@ -95,7 +95,7 @@ def run(self):
user = key
tmp_data = {}
- tmp_data["user"] = '
' + user
+ tmp_data["user"] = '
' + escape_html(user)
grid_list_local_admin_targets_data = []
grid_list_domain_admin_targets_data = []
# create the grid
@@ -114,7 +114,7 @@ def run(self):
local_distinct_ends = []
for domain in data_local_admins[key]:
list_local_admin_targets_tmp_data = {
- "domain": '
' + domain
+ "domain": '
' + escape_html(domain)
}
numberofpaths = 0
for path in data_local_admins[key][domain]:
@@ -194,7 +194,7 @@ def run(self):
domain_distinct_ends = []
for domain in data_domain_admins[key]:
list_domain_admin_targets_tmp_data = {
- "domain": '
' + domain
+ "domain": '
' + escape_html(domain)
}
for path in data_domain_admins[key][domain]:
diff --git a/ad_miner/sources/modules/controls/da_to_da.py b/ad_miner/sources/modules/controls/da_to_da.py
index 4666df24..f234fe2e 100644
--- a/ad_miner/sources/modules/controls/da_to_da.py
+++ b/ad_miner/sources/modules/controls/da_to_da.py
@@ -3,7 +3,7 @@
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.grid_class import Grid
from ad_miner.sources.modules.graph_class import Graph
-from ad_miner.sources.modules.utils import grid_data_stringify
+from ad_miner.sources.modules.utils import grid_data_stringify, escape_html
from ad_miner.sources.modules.common_analysis import presence_of
from urllib.parse import quote
@@ -57,7 +57,7 @@ def run(self):
headers.append(domain)
graphDatas[domain] = {}
pathLengthss.append(
- {"FROM / TO": '
' + domain, domain: "-"}
+ {"FROM / TO": '
' + escape_html(domain), domain: "-"}
)
for path in paths:
# headers and pathLengths share the same index and it is cheaper to use headers here
@@ -70,7 +70,7 @@ def run(self):
graphDatas[unknown_domain] = {}
pathLengthss.append(
{
- "FROM / TO": '
' + unknown_domain,
+ "FROM / TO": '
' + escape_html(unknown_domain),
unknown_domain: "-",
}
)
diff --git a/ad_miner/sources/modules/controls/dom_admin_on_non_dc.py b/ad_miner/sources/modules/controls/dom_admin_on_non_dc.py
index 6dcc10bf..de0ab539 100644
--- a/ad_miner/sources/modules/controls/dom_admin_on_non_dc.py
+++ b/ad_miner/sources/modules/controls/dom_admin_on_non_dc.py
@@ -2,7 +2,7 @@
from ad_miner.sources.modules.controls import register_control
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.grid_class import Grid
-from ad_miner.sources.modules.utils import grid_data_stringify
+from ad_miner.sources.modules.utils import grid_data_stringify, escape_html
from ad_miner.sources.modules.common_analysis import createGraphPage, presence_of
@@ -57,8 +57,8 @@ def run(self):
data = []
for da in dico.keys():
tmp = {}
- tmp["Domain"] = '
' + dico[da]["domain"]
- tmp["Domain Admin"] = '
' + da
+ tmp["Domain"] = '
' + escape_html(dico[da]["domain"])
+ tmp["Domain Admin"] = '
' + escape_html(da)
nb_computers = len(dico[da]["computers"])
tmp["Computers"] = grid_data_stringify(
{
@@ -103,7 +103,7 @@ def run(self):
)
computer_list_grid.setheaders(["Computer"])
computer_list_data = [
- {"Computer": '
' + c}
+ {"Computer": '
' + escape_html(c)}
for c in dico[da]["computers"]
]
computer_list_grid.setData(computer_list_data)
@@ -122,7 +122,7 @@ def run(self):
)
domain_list_grid.setheaders(["Domain"])
domain_list_data = [
- {"Domain": '
' + c} for c in domain_list
+ {"Domain": '
' + escape_html(c)} for c in domain_list
]
domain_list_grid.setData(domain_list_data)
domain_list_page.addComponent(domain_list_grid)
diff --git a/ad_miner/sources/modules/controls/dormants_accounts.py b/ad_miner/sources/modules/controls/dormants_accounts.py
index 1ca607d9..82872ef0 100644
--- a/ad_miner/sources/modules/controls/dormants_accounts.py
+++ b/ad_miner/sources/modules/controls/dormants_accounts.py
@@ -3,7 +3,7 @@
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.grid_class import Grid
-from ad_miner.sources.modules.utils import days_format
+from ad_miner.sources.modules.utils import days_format, escape_html
from ad_miner.sources.modules.common_analysis import percentage_superior
@@ -50,15 +50,15 @@ def run(self):
data = []
for dict in self.users_dormant_accounts:
- tmp_data = {"domain": '
' + dict["domain"]}
+ tmp_data = {"domain": '
' + escape_html(dict["domain"])}
tmp_data["name"] = (
(
'
'
- + dict["name"]
+ + escape_html(dict["name"])
)
if dict["name"] in self.admin_list
- else '
' + dict["name"]
+ else '
' + escape_html(dict["name"])
)
tmp_data["last logon"] = days_format(dict["days"])
diff --git a/ad_miner/sources/modules/controls/empty_groups.py b/ad_miner/sources/modules/controls/empty_groups.py
index d796719d..476963de 100644
--- a/ad_miner/sources/modules/controls/empty_groups.py
+++ b/ad_miner/sources/modules/controls/empty_groups.py
@@ -3,6 +3,7 @@
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.grid_class import Grid
+from ad_miner.sources.modules.utils import escape_html
@register_control
@@ -35,7 +36,7 @@ def run(self):
headers = ["Empty group", "Full Reference"]
for d in self.empty_groups:
- d["Empty group"] = '
' + d["Empty group"]
+ d["Empty group"] = '
' + escape_html(d["Empty group"])
grid.setheaders(headers)
grid.setData(self.empty_groups)
diff --git a/ad_miner/sources/modules/controls/empty_ous.py b/ad_miner/sources/modules/controls/empty_ous.py
index a1c6af6b..6c379721 100644
--- a/ad_miner/sources/modules/controls/empty_ous.py
+++ b/ad_miner/sources/modules/controls/empty_ous.py
@@ -3,6 +3,7 @@
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.grid_class import Grid
+from ad_miner.sources.modules.utils import escape_html
@register_control
@@ -36,7 +37,7 @@ def run(self):
for d in self.empty_ous:
d["Empty Organizational Unit"] = (
- '
' + d["Empty Organizational Unit"]
+ '
' + escape_html(d["Empty Organizational Unit"])
)
grid.setheaders(headers)
diff --git a/ad_miner/sources/modules/controls/graph_list_objects_rbcd.py b/ad_miner/sources/modules/controls/graph_list_objects_rbcd.py
index 66b48e0a..6cdffdce 100644
--- a/ad_miner/sources/modules/controls/graph_list_objects_rbcd.py
+++ b/ad_miner/sources/modules/controls/graph_list_objects_rbcd.py
@@ -4,7 +4,7 @@
from ad_miner.sources.modules import logger
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.grid_class import Grid
-from ad_miner.sources.modules.utils import grid_data_stringify
+from ad_miner.sources.modules.utils import grid_data_stringify, escape_html
from ad_miner.sources.modules.common_analysis import presence_of, createGraphPage
from urllib.parse import quote
@@ -161,9 +161,9 @@ def run(self):
tmp_data = {}
tmp_data["Domain"] = (
'
'
- + self.rbcd_graphs[object_name]["domain"]
+ + escape_html(self.rbcd_graphs[object_name]["domain"])
)
- tmp_data["Name"] = '
' + object_name
+ tmp_data["Name"] = '
' + escape_html(object_name)
sortClass1 = str(len(self.rbcd_graphs[object_name]["paths"])).zfill(6)
tmp_data["Paths to targets"] = grid_data_stringify(
{
diff --git a/ad_miner/sources/modules/controls/graph_path_objects_to_da.py b/ad_miner/sources/modules/controls/graph_path_objects_to_da.py
index e6aeece0..b7489a66 100644
--- a/ad_miner/sources/modules/controls/graph_path_objects_to_da.py
+++ b/ad_miner/sources/modules/controls/graph_path_objects_to_da.py
@@ -4,7 +4,7 @@
from ad_miner.sources.modules import logger
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.grid_class import Grid
-from ad_miner.sources.modules.utils import grid_data_stringify
+from ad_miner.sources.modules.utils import grid_data_stringify, escape_html
from ad_miner.sources.modules.common_analysis import presence_of, createGraphPage
from urllib.parse import quote
@@ -167,7 +167,7 @@ def count_object_from_path(list_of_paths):
domain = domain[0]
tmp_data = {}
- tmp_data[headers[0]] = '
' + domain
+ tmp_data[headers[0]] = '
' + escape_html(domain)
count = count_object_from_path(self.users_to_domain[domain])
sortClass = str(count).zfill(
diff --git a/ad_miner/sources/modules/controls/guest_accounts.py b/ad_miner/sources/modules/controls/guest_accounts.py
index 6636303c..81c72813 100644
--- a/ad_miner/sources/modules/controls/guest_accounts.py
+++ b/ad_miner/sources/modules/controls/guest_accounts.py
@@ -3,6 +3,7 @@
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.grid_class import Grid
from ad_miner.sources.modules.common_analysis import presence_of
+from ad_miner.sources.modules.utils import escape_html
@register_control
@@ -39,8 +40,8 @@ def run(self):
data = []
for account_name, domain, is_enabled in guest_list:
- tmp_data = {"domain": '
' + domain}
- tmp_data["name"] = '
' + account_name
+ tmp_data = {"domain": '
' + escape_html(domain)}
+ tmp_data["name"] = '
' + escape_html(account_name)
tmp_data["enabled"] = (
'
Enabled'
if is_enabled
diff --git a/ad_miner/sources/modules/controls/kerberoastables.py b/ad_miner/sources/modules/controls/kerberoastables.py
index c0d196e9..e55c5733 100644
--- a/ad_miner/sources/modules/controls/kerberoastables.py
+++ b/ad_miner/sources/modules/controls/kerberoastables.py
@@ -3,7 +3,7 @@
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.grid_class import Grid
-from ad_miner.sources.modules.utils import grid_data_stringify, days_format
+from ad_miner.sources.modules.utils import grid_data_stringify, days_format, escape_html
from ad_miner.sources.modules.common_analysis import containsDAs
from urllib.parse import quote
@@ -88,17 +88,17 @@ def run(self):
if self.users_kerberoastable_users[elem]["is_Domain_Admin"] == True:
self.users_kerberoastable_users[elem]["name"] = (
'
'
- + self.users_kerberoastable_users[elem]["name"]
+ + escape_html(self.users_kerberoastable_users[elem]["name"])
)
else:
self.users_kerberoastable_users[elem]["name"] = (
'
'
- + self.users_kerberoastable_users[elem]["name"]
+ + escape_html(self.users_kerberoastable_users[elem]["name"])
)
data = []
for dict in self.users_kerberoastable_users:
- tmp_data = {"domain": '
' + dict["domain"]}
+ tmp_data = {"domain": '
' + escape_html(dict["domain"])}
tmp_data["name"] = dict["name"]
tmp_data["Last password change"] = days_format(dict["pass_last_change"])
tmp_data["Account Creation Date"] = days_format(dict["accountCreationDate"])
diff --git a/ad_miner/sources/modules/controls/krb_last_change.py b/ad_miner/sources/modules/controls/krb_last_change.py
index 26bf026b..5738f136 100644
--- a/ad_miner/sources/modules/controls/krb_last_change.py
+++ b/ad_miner/sources/modules/controls/krb_last_change.py
@@ -3,7 +3,7 @@
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.grid_class import Grid
-from ad_miner.sources.modules.utils import days_format
+from ad_miner.sources.modules.utils import days_format, escape_html
from ad_miner.sources.modules.common_analysis import time_since
@@ -41,9 +41,9 @@ def run(self):
data = []
for dict in self.users_krb_pwd_last_set:
- tmp_data = {"domain": '
' + dict["domain"]}
+ tmp_data = {"domain": '
' + escape_html(dict["domain"])}
tmp_data["name"] = (
- '
' + dict["name"]
+ '
' + escape_html(dict["name"])
)
tmp_data["Last password change"] = days_format(dict["pass_last_change"])
tmp_data["Account Creation Date"] = days_format(dict["accountCreationDate"])
diff --git a/ad_miner/sources/modules/controls/nb_domain_admins.py b/ad_miner/sources/modules/controls/nb_domain_admins.py
index bbeb2174..2d0a24b2 100644
--- a/ad_miner/sources/modules/controls/nb_domain_admins.py
+++ b/ad_miner/sources/modules/controls/nb_domain_admins.py
@@ -5,6 +5,7 @@
from ad_miner.sources.modules.grid_class import Grid
from ad_miner.sources.modules.common_analysis import presence_of
+from ad_miner.sources.modules.utils import escape_html
@register_control
@@ -48,8 +49,8 @@ def run(self):
for da in self.users_nb_domain_admins:
tmp_data = {}
- tmp_data["domain"] = '
' + da["domain"]
- tmp_data["name"] = '
' + da["name"]
+ tmp_data["domain"] = '
' + escape_html(da["domain"])
+ tmp_data["name"] = '
' + escape_html(da["name"])
tmp_data["domain admin"] = (
'
True '
if "Domain Admin" in da["admin type"]
diff --git a/ad_miner/sources/modules/controls/never_expires.py b/ad_miner/sources/modules/controls/never_expires.py
index 1671808a..83af610f 100644
--- a/ad_miner/sources/modules/controls/never_expires.py
+++ b/ad_miner/sources/modules/controls/never_expires.py
@@ -4,7 +4,7 @@
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.grid_class import Grid
-from ad_miner.sources.modules.utils import days_format
+from ad_miner.sources.modules.utils import days_format, escape_html
from ad_miner.sources.modules.common_analysis import percentage_superior
@@ -42,10 +42,10 @@ def run(self):
if user["name"] in self.admin_list:
user["name"] = (
'
'
- + user["name"]
+ + escape_html(user["name"])
)
else:
- user["name"] = '
' + user["name"]
+ user["name"] = '
' + escape_html(user["name"])
page = Page(
self.arguments.cache_prefix,
"never_expires",
@@ -66,7 +66,7 @@ def run(self):
data = []
for dict in self.users_password_never_expires:
tmp_data = {
- "domain": '
' + dict["domain"],
+ "domain": '
' + escape_html(dict["domain"]),
"name": dict["name"],
}
tmp_data["Last login"] = days_format(dict["LastLogin"])
diff --git a/ad_miner/sources/modules/controls/objects_to_adcs.py b/ad_miner/sources/modules/controls/objects_to_adcs.py
index ceadab46..dd23d7e6 100644
--- a/ad_miner/sources/modules/controls/objects_to_adcs.py
+++ b/ad_miner/sources/modules/controls/objects_to_adcs.py
@@ -5,7 +5,7 @@
from ad_miner.sources.modules.grid_class import Grid
from ad_miner.sources.modules.graph_class import Graph
-from ad_miner.sources.modules.utils import grid_data_stringify
+from ad_miner.sources.modules.utils import grid_data_stringify, escape_html
from ad_miner.sources.modules.common_analysis import presence_of
from urllib.parse import quote
@@ -71,9 +71,9 @@ def run(self):
for key, paths in self.ADCS_path_sorted.items():
tmp_data = {}
tmp_data["Domain"] = (
- '
' + paths[0].nodes[-1].domain
+ '
' + escape_html(paths[0].nodes[-1].domain)
)
- tmp_data["Name"] = '
' + key
+ tmp_data["Name"] = '
' + escape_html(key)
nb_path_to_adcs = len(paths)
self.total_paths += nb_path_to_adcs
sortClass = str(nb_path_to_adcs).zfill(6)
diff --git a/ad_miner/sources/modules/controls/objects_to_operators_member.py b/ad_miner/sources/modules/controls/objects_to_operators_member.py
index 62c27188..6c44dcb7 100644
--- a/ad_miner/sources/modules/controls/objects_to_operators_member.py
+++ b/ad_miner/sources/modules/controls/objects_to_operators_member.py
@@ -4,7 +4,7 @@
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.grid_class import Grid
from ad_miner.sources.modules.graph_class import Graph
-from ad_miner.sources.modules.utils import grid_data_stringify
+from ad_miner.sources.modules.utils import grid_data_stringify, escape_html
from ad_miner.sources.modules.common_analysis import presence_of
from urllib.parse import quote
@@ -49,8 +49,8 @@ def run(self):
data[path.nodes[0].name]["target"].append(path.nodes[-1].name)
except KeyError:
data[path.nodes[0].name] = {
- "domain": '
' + path.nodes[-1].domain,
- "name": '
' + path.nodes[0].name,
+ "domain": '
' + escape_html(path.nodes[-1].domain),
+ "name": '
' + escape_html(path.nodes[0].name),
"link": quote(str(path.nodes[0].name)),
"target": [path.nodes[-1].name],
"paths": [path],
@@ -63,8 +63,8 @@ def run(self):
KeyError
): # Really **should not** happen, but to prevent crash in case of corrupted cache/db
data[path.nodes[-1].name] = {
- "domain": '
' + path.nodes[-1].domain,
- "name": '
' + path.nodes[-1].name,
+ "domain": '
' + escape_html(path.nodes[-1].domain),
+ "name": '
' + escape_html(path.nodes[-1].name),
"link": quote(str(path.nodes[-1].name)),
"target": [""],
"paths": [path],
diff --git a/ad_miner/sources/modules/controls/pre_windows_2000_compatible_access_group.py b/ad_miner/sources/modules/controls/pre_windows_2000_compatible_access_group.py
index c9f43c00..fc2e4e95 100644
--- a/ad_miner/sources/modules/controls/pre_windows_2000_compatible_access_group.py
+++ b/ad_miner/sources/modules/controls/pre_windows_2000_compatible_access_group.py
@@ -4,6 +4,7 @@
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.grid_class import Grid
from ad_miner.sources.modules import generic_formating
+from ad_miner.sources.modules.utils import escape_html
@register_control
@@ -56,12 +57,12 @@ def run(self):
data = []
for domain, account_name, objectid, type_list in sorted_list:
- tmp_data = {"Domain": '
' + domain}
+ tmp_data = {"Domain": '
' + escape_html(domain)}
type_clean = generic_formating.clean_label(type_list)
tmp_data["Name"] = (
- f"{generic_formating.get_label_icon(type_clean)} {account_name}"
+ f"{generic_formating.get_label_icon(type_clean)} {escape_html(account_name)}"
)
tmp_data["Rating"] = (
diff --git a/ad_miner/sources/modules/controls/primaryGroupID_lower_than_1000.py b/ad_miner/sources/modules/controls/primaryGroupID_lower_than_1000.py
index 7dbf7abd..10d040e8 100644
--- a/ad_miner/sources/modules/controls/primaryGroupID_lower_than_1000.py
+++ b/ad_miner/sources/modules/controls/primaryGroupID_lower_than_1000.py
@@ -3,7 +3,7 @@
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.grid_class import Grid
-from ad_miner.sources.modules.utils import MODULES_DIRECTORY
+from ad_miner.sources.modules.utils import MODULES_DIRECTORY, escape_html
import json
@@ -54,21 +54,21 @@ def run(self):
tmp_data = {}
if str(rid) not in known_RIDs:
- tmp_data["domain"] = '
' + domain
+ tmp_data["domain"] = '
' + escape_html(domain)
tmp_data["RID"] = str(rid)
tmp_data["name"] = (
- '
' + name if is_da else name
+ '
' + escape_html(name) if is_da else escape_html(name)
)
tmp_data["reason"] = "Unknown RID"
data.append(tmp_data)
elif name_without_domain not in known_RIDs[str(rid)]:
- tmp_data["domain"] = '
' + domain
+ tmp_data["domain"] = '
' + escape_html(domain)
tmp_data["RID"] = str(rid)
tmp_data["name"] = (
- '
' + name if is_da else name
+ '
' + escape_html(name) if is_da else escape_html(name)
)
tmp_data["reason"] = (
- "Unexpected name, expected : " + known_RIDs[str(rid)][0]
+ "Unexpected name, expected : " + escape_html(known_RIDs[str(rid)][0])
)
data.append(tmp_data)
diff --git a/ad_miner/sources/modules/controls/privileged_accounts_outside_Protected_Users.py b/ad_miner/sources/modules/controls/privileged_accounts_outside_Protected_Users.py
index 4e2dbcec..8f1a1f5d 100644
--- a/ad_miner/sources/modules/controls/privileged_accounts_outside_Protected_Users.py
+++ b/ad_miner/sources/modules/controls/privileged_accounts_outside_Protected_Users.py
@@ -3,6 +3,7 @@
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.grid_class import Grid
from ad_miner.sources.modules.common_analysis import presence_of
+from ad_miner.sources.modules.utils import escape_html
@register_control
@@ -55,8 +56,8 @@ def run(self):
for dic in self.users_nb_domain_admins:
if "Protected Users" in dic["admin type"]:
continue
- tmp_data = {"domain": '
' + dic["domain"]}
- tmp_data["name"] = '
' + dic["name"]
+ tmp_data = {"domain": '
' + escape_html(dic["domain"])}
+ tmp_data["name"] = '
' + escape_html(dic["name"])
tmp_data["domain admin"] = (
'
True '
if "Domain Admin" in dic["admin type"]
diff --git a/ad_miner/sources/modules/controls/up_to_date_admincount.py b/ad_miner/sources/modules/controls/up_to_date_admincount.py
index 3f6558ff..1963ed00 100644
--- a/ad_miner/sources/modules/controls/up_to_date_admincount.py
+++ b/ad_miner/sources/modules/controls/up_to_date_admincount.py
@@ -2,6 +2,7 @@
from ad_miner.sources.modules.controls import register_control
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.grid_class import Grid
+from ad_miner.sources.modules.utils import escape_html
@register_control
@@ -56,8 +57,8 @@ def run(self):
for dic in self.users_nb_domain_admins:
if dic["admincount"]:
continue
- tmp_data = {"domain": '
' + dic["domain"]}
- tmp_data["name"] = '
' + dic["name"]
+ tmp_data = {"domain": '
' + escape_html(dic["domain"])}
+ tmp_data["name"] = '
' + escape_html(dic["name"])
tmp_data["domain admin"] = (
'
True '
if "Domain Admin" in dic["admin type"]
@@ -94,8 +95,8 @@ def run(self):
data.append(tmp_data)
for name, domain, da_type in self.unpriviledged_users_with_admincount:
- tmp_data = {"domain": '
' + domain}
- tmp_data["name"] = '
' + name
+ tmp_data = {"domain": '
' + escape_html(domain)}
+ tmp_data["name"] = '
' + escape_html(name)
tmp_data["domain admin"] = '
'
tmp_data["schema admin"] = '
'
tmp_data["enterprise admin"] = '
'
diff --git a/ad_miner/sources/modules/controls/users_admin_of_computers.py b/ad_miner/sources/modules/controls/users_admin_of_computers.py
index ca6e7c0f..28b34124 100644
--- a/ad_miner/sources/modules/controls/users_admin_of_computers.py
+++ b/ad_miner/sources/modules/controls/users_admin_of_computers.py
@@ -8,7 +8,7 @@
from ad_miner.sources.modules.path_neo4j import Path
from ad_miner.sources.modules import generic_computing
-from ad_miner.sources.modules.utils import grid_data_stringify
+from ad_miner.sources.modules.utils import grid_data_stringify, escape_html
from ad_miner.sources.modules.common_analysis import (
findAndCreatePathToDaFromUsersList,
hasPathToDA,
@@ -306,7 +306,7 @@ def get_last_pass_change(account):
headers[
0
]: '
'
- + dict[headers[0]],
+ + escape_html(dict[headers[0]]),
headers[1]: dict[headers[1]],
headers[2]: dict[headers[2]],
headers[3]: data_header_computer,
@@ -318,7 +318,7 @@ def get_last_pass_change(account):
formated_data.append(
{
headers[0]: '
'
- + dict[headers[0]],
+ + escape_html(dict[headers[0]]),
headers[1]: dict[headers[1]],
headers[2]: dict[headers[2]],
headers[3]: data_header_computer,
diff --git a/ad_miner/sources/modules/controls/users_password_not_required.py b/ad_miner/sources/modules/controls/users_password_not_required.py
index faaac8bc..eff90236 100644
--- a/ad_miner/sources/modules/controls/users_password_not_required.py
+++ b/ad_miner/sources/modules/controls/users_password_not_required.py
@@ -4,7 +4,7 @@
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.grid_class import Grid
-from ad_miner.sources.modules.utils import days_format
+from ad_miner.sources.modules.utils import days_format, escape_html
from ad_miner.sources.modules.common_analysis import presence_of
@@ -41,8 +41,8 @@ def run(self):
grid_data = []
for dic in self.users_password_not_required:
tmp_data = {}
- tmp_data["Domain"] = '
' + dic["domain"]
- tmp_data["User"] = '
' + dic["user"]
+ tmp_data["Domain"] = '
' + escape_html(dic["domain"])
+ tmp_data["User"] = '
' + escape_html(dic["user"])
tmp_data["Password last change"] = days_format(dic["pwdlastset"])
tmp_data["Last logon"] = days_format(dic["lastlogon"])
grid_data.append(tmp_data)
diff --git a/ad_miner/sources/modules/controls/users_pwd_not_changed_since.py b/ad_miner/sources/modules/controls/users_pwd_not_changed_since.py
index e949231b..6adaa259 100644
--- a/ad_miner/sources/modules/controls/users_pwd_not_changed_since.py
+++ b/ad_miner/sources/modules/controls/users_pwd_not_changed_since.py
@@ -4,7 +4,7 @@
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.grid_class import Grid
-from ad_miner.sources.modules.utils import days_format
+from ad_miner.sources.modules.utils import days_format, escape_html
from ad_miner.sources.modules.common_analysis import percentage_superior
@@ -69,11 +69,11 @@ def run(self):
if dict["user"] in self.admin_list:
tmp_data["user"] = (
'
'
- + tmp_data["user"]
+ + escape_html(tmp_data["user"])
)
else:
tmp_data["user"] = (
- '
' + tmp_data["user"]
+ '
' + escape_html(tmp_data["user"])
)
tmp_data["Last password change"] = days_format(dict["days"])
tmp_data["Account Creation Date"] = days_format(dict["accountCreationDate"])
diff --git a/ad_miner/sources/modules/controls/users_rdp_access.py b/ad_miner/sources/modules/controls/users_rdp_access.py
index 7e46aa18..de05e38c 100644
--- a/ad_miner/sources/modules/controls/users_rdp_access.py
+++ b/ad_miner/sources/modules/controls/users_rdp_access.py
@@ -3,7 +3,7 @@
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.grid_class import Grid
-from ad_miner.sources.modules.utils import grid_data_stringify
+from ad_miner.sources.modules.utils import grid_data_stringify, escape_html
from ad_miner.sources.modules.common_analysis import percentage_superior
from urllib.parse import quote
@@ -48,7 +48,7 @@ def run(self):
for key in self.users_rdp_access_1:
sortClass = str(len(self.users_rdp_access_1[key])).zfill(6)
d = {
- "Users": '
' + key,
+ "Users": '
' + escape_html(key),
"Computers": grid_data_stringify(
{
"value": f"{len(self.users_rdp_access_1[key])} Computers
{self.users_rdp_access_1[key]}
",
diff --git a/ad_miner/sources/modules/controls/users_shadow_credentials.py b/ad_miner/sources/modules/controls/users_shadow_credentials.py
index cfa79824..e1c9db78 100644
--- a/ad_miner/sources/modules/controls/users_shadow_credentials.py
+++ b/ad_miner/sources/modules/controls/users_shadow_credentials.py
@@ -5,7 +5,7 @@
from ad_miner.sources.modules.grid_class import Grid
from ad_miner.sources.modules.graph_class import Graph
-from ad_miner.sources.modules.utils import grid_data_stringify
+from ad_miner.sources.modules.utils import grid_data_stringify, escape_html
from ad_miner.sources.modules.common_analysis import presence_of
from urllib.parse import quote
@@ -59,8 +59,8 @@ def run(self):
for d in data.values():
sortClass = str(len(d["paths"])).zfill(6)
tmp_grid_data = {
- "domain": '
' + d["domain"],
- "name": '
' + d["name"],
+ "domain": '
' + escape_html(d["domain"]),
+ "name": '
' + escape_html(d["name"]),
"target": grid_data_stringify(
{
"value": f"{len(d['paths'])} paths to {len(d['target'])} target{'s' if len(d['target'])>1 else ''}",
diff --git a/ad_miner/sources/modules/controls/users_shadow_credentials_to_non_admins.py b/ad_miner/sources/modules/controls/users_shadow_credentials_to_non_admins.py
index a51bae38..9de0e0b2 100644
--- a/ad_miner/sources/modules/controls/users_shadow_credentials_to_non_admins.py
+++ b/ad_miner/sources/modules/controls/users_shadow_credentials_to_non_admins.py
@@ -4,7 +4,7 @@
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.graph_class import Graph
from ad_miner.sources.modules.grid_class import Grid
-from ad_miner.sources.modules.utils import grid_data_stringify
+from ad_miner.sources.modules.utils import grid_data_stringify, escape_html
from ad_miner.sources.modules.common_analysis import presence_of
@@ -53,9 +53,9 @@ def run(self):
sortClass = str(nb_paths).zfill(6)
grid_data.append(
{
- "domain": '
' + data[target]["domain"],
+ "domain": '
' + escape_html(data[target]["domain"]),
"target": '
'
- + data[target]["target"],
+ + escape_html(data[target]["target"]),
"paths": grid_data_stringify(
{
"value": f"{nb_paths} paths to target",
diff --git a/ad_miner/sources/modules/controls/vuln_functional_level.py b/ad_miner/sources/modules/controls/vuln_functional_level.py
index e01ac81e..d7800d97 100644
--- a/ad_miner/sources/modules/controls/vuln_functional_level.py
+++ b/ad_miner/sources/modules/controls/vuln_functional_level.py
@@ -3,6 +3,7 @@
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.grid_class import Grid
+from ad_miner.sources.modules.utils import escape_html
@register_control
diff --git a/ad_miner/sources/modules/main_page.py b/ad_miner/sources/modules/main_page.py
index ec12526d..5d48934d 100644
--- a/ad_miner/sources/modules/main_page.py
+++ b/ad_miner/sources/modules/main_page.py
@@ -8,7 +8,7 @@
from ad_miner.sources.modules import common_analysis
from ad_miner.sources.modules.smolcard_class import SmolCard
-from ad_miner.sources.modules.utils import TEMPLATES_DIRECTORY
+from ad_miner.sources.modules.utils import TEMPLATES_DIRECTORY, escape_html
def americanStyle(n: int) -> str:
@@ -562,14 +562,16 @@ def render(
] = ""
red_status = f"""
{risk_control.replace("_", " ").capitalize()}"""
for issue in data[category_repartition][f"{risk_control}_list"]:
- custom_title = dico_name_description[issue].replace("$", "")
+ custom_title = escape_html(dico_name_description[issue].replace("$", ""))
+ card_title = escape_html(dico_name_title[issue])
+ escaped_red_status = escape_html(red_status)
data[category_repartition][
global_risk_controls[risk_control]["panel_key"]
] += f"""
-
+
-
{dico_name_title[issue]}
+ {card_title}
diff --git a/ad_miner/sources/modules/safe_pickle.py b/ad_miner/sources/modules/safe_pickle.py
new file mode 100644
index 00000000..6c83ebf5
--- /dev/null
+++ b/ad_miner/sources/modules/safe_pickle.py
@@ -0,0 +1,53 @@
+"""
+Secure pickle deserialization module.
+
+This module provides a RestrictedUnpickler that only allows deserialization
+of safe, whitelisted classes to prevent arbitrary code execution attacks.
+"""
+
+import pickle
+
+# Whitelist of allowed modules and classes for deserialization
+SAFE_MODULES = {
+ 'builtins': {'dict', 'list', 'set', 'frozenset', 'tuple', 'str', 'int', 'float', 'bool', 'bytes', 'type', 'NoneType'},
+ 'datetime': {'datetime', 'date', 'time', 'timedelta', 'timezone'},
+ 'neo4j.time': {'DateTime', 'Date', 'Time', 'Duration'},
+ 'neo4j.graph': {'Node', 'Relationship', 'Path'},
+ 'collections': {'OrderedDict', 'defaultdict'},
+ # AD_Miner internal data classes (safe - no dangerous methods)
+ 'ad_miner.sources.modules.path_neo4j': {'Path'},
+ 'ad_miner.sources.modules.node_neo4j': {'Node'},
+}
+
+
+class RestrictedUnpickler(pickle.Unpickler):
+ """
+ A restricted unpickler that only allows deserialization of whitelisted classes.
+
+ This prevents arbitrary code execution through malicious pickle files by
+ raising an error when an unauthorized class is encountered.
+ """
+
+ def find_class(self, module, name):
+ if module in SAFE_MODULES and name in SAFE_MODULES[module]:
+ return super().find_class(module, name)
+ raise pickle.UnpicklingError(
+ f"Unauthorized class: {module}.{name}. "
+ "Only whitelisted classes can be deserialized for security reasons."
+ )
+
+
+def safe_load(file):
+ """
+ Safely load a pickle file using the RestrictedUnpickler.
+
+ Args:
+ file: A file-like object opened in binary mode.
+
+ Returns:
+ The deserialized Python object.
+
+ Raises:
+ pickle.UnpicklingError: If an unauthorized class is encountered.
+ """
+ return RestrictedUnpickler(file).load()
diff --git a/ad_miner/sources/modules/table_class.py b/ad_miner/sources/modules/table_class.py
index d269d080..4f59b8a9 100755
--- a/ad_miner/sources/modules/table_class.py
+++ b/ad_miner/sources/modules/table_class.py
@@ -1,7 +1,7 @@
import random
import string
-from ad_miner.sources.modules.utils import HTML_DIRECTORY
+from ad_miner.sources.modules.utils import HTML_DIRECTORY, escape_html
class Table:
def __init__(
@@ -41,7 +41,7 @@ def render(self, page_f):
with open(
self.template_base_path / (self.template + "_header.html"), "r"
) as header_f:
- page_f.write(header_f.read() % (self.id, self.id, self.title, self.id))
+ page_f.write(header_f.read() % (self.id, self.id, escape_html(self.title), self.id))
page_f.write('\n\n')
@@ -51,7 +51,7 @@ def render(self, page_f):
line = row_header_f.read()
for header in self.headers:
- page_f.write(line % ("th", 'scope="col"', header, "th"))
+ page_f.write(line % ("th", 'scope="col"', escape_html(header), "th"))
page_f.write(" \n \n\n")
@@ -60,10 +60,11 @@ def render(self, page_f):
page_f.write("\n")
for index, col in enumerate(row):
+ escaped_col = escape_html(col)
if index == 0:
- page_f.write(line % ("th", 'scope="row"', col, "th"))
+ page_f.write(line % ("th", 'scope="row"', escaped_col, "th"))
else:
- page_f.write(line % ("td", "", col, "td"))
+ page_f.write(line % ("td", "", escaped_col, "td"))
page_f.write(" \n")
page_f.write(" \n")
diff --git a/ad_miner/sources/modules/utils.py b/ad_miner/sources/modules/utils.py
index 3468b7bb..882dc16b 100755
--- a/ad_miner/sources/modules/utils.py
+++ b/ad_miner/sources/modules/utils.py
@@ -1,4 +1,5 @@
import argparse
+import html
from pathlib import Path
import multiprocessing as mp
import json
@@ -6,6 +7,21 @@
from datetime import date
from os.path import sep
+
+def escape_html(text):
+ """
+ Escape HTML special characters to prevent XSS attacks.
+
+ Args:
+ text: The text to escape. Can be None or any type.
+
+ Returns:
+ A string with HTML special characters escaped.
+ """
+ if text is None:
+ return ""
+ return html.escape(str(text), quote=True).replace("'", "'")
+
today = date.today()
current_date = today.strftime("%Y%m%d")
@@ -171,11 +187,13 @@ def grid_data_stringify(raw_data: dict) -> str:
"before_link"
}
"""
- link = raw_data['link'].replace(sep, '_').replace('/', '_')
+ link = escape_html(raw_data['link'].replace(sep, '_').replace('/', '_'))
+ escaped_value = escape_html(raw_data['value'])
try:
- return f"{raw_data['before_link']} {raw_data['value']} "
+ escaped_before = escape_html(raw_data['before_link'])
+ return f"{escaped_before} {escaped_value} "
except KeyError:
- return f"{raw_data['value']} "
+ return f"{escaped_value} "
def cache_check(template:str,cache:bool) -> dict:
res={'nb_cache':0}