nh3 clean is now used on all free-text fields to ensure only approved html will be saved to the database. #72

This commit is contained in:
Liam Brydon
2025-10-21 10:09:10 +13:00
parent 43bce8a4e1
commit 61346c4097
2 changed files with 101 additions and 73 deletions

View File

@ -12,6 +12,8 @@ def classname(obj):
@register.filter(name="nh_safe") @register.filter(name="nh_safe")
def nh_safe(txt: str): def nh_safe(txt):
if not isinstance(txt, str):
return txt
clean_html = nh3.clean(txt, tags=s.ALLOWED_HTML_TAGS) clean_html = nh3.clean(txt, tags=s.ALLOWED_HTML_TAGS)
return mark_safe(clean_html) return mark_safe(clean_html)

View File

@ -58,14 +58,14 @@ def log_post_params(request):
logger.debug(f"{k}\t{v}") logger.debug(f"{k}\t{v}")
def sanitize_dict(bad_dict): def clean_dict(bad_dict):
"""Check each value in a dictionary for XSS attempts""" """Check each value in a dictionary for XSS attempts"""
clean_dict = {} # TODO: I'm not sure if this is the best way to do this. clean = {} # TODO: I'm not sure if this is the best way to do this.
for key, value in bad_dict.items(): for key, value in bad_dict.items():
if key != 'csrfmiddlewaretoken' and isinstance(value, str): if key != 'csrfmiddlewaretoken' and isinstance(value, str):
value = nh3.clean(value, tags=s.ALLOWED_HTML_TAGS) value = nh3.clean(value, tags=s.ALLOWED_HTML_TAGS)
clean_dict[key] = value clean[key] = value
return clean_dict return clean
def error(request, message: str, detail: str, code: int = 400): def error(request, message: str, detail: str, code: int = 400):
@ -400,11 +400,11 @@ def packages(request):
else: else:
return HttpResponseBadRequest() return HttpResponseBadRequest()
else: else:
package_name = request.POST.get("package-name") # Clean for potential XSS
# Sanitize for potential XSS package_name = nh3.clean(request.POST.get("package-name"), tags=s.ALLOWED_HTML_TAGS).strip()
package_description = nh3.clean(request.POST.get( package_description = nh3.clean(request.POST.get(
"package-description", s.DEFAULT_VALUES["description"] "package-description", s.DEFAULT_VALUES["description"]
), tags=s.ALLOWED_HTML_TAGS) ), tags=s.ALLOWED_HTML_TAGS).strip()
created_package = PackageManager.create_package( created_package = PackageManager.create_package(
current_user, package_name, package_description current_user, package_name, package_description
@ -679,7 +679,9 @@ def search(request):
if request.method == "GET": if request.method == "GET":
package_urls = request.GET.getlist("packages") package_urls = request.GET.getlist("packages")
searchterm = request.GET.get("search") # Clean for potential XSS
searchterm = nh3.clean(request.GET.get("search"), tags=s.ALLOWED_HTML_TAGS).strip()
mode = request.GET.get("mode") mode = request.GET.get("mode")
# add HTTP_ACCEPT check to differentiate between index and ajax call # add HTTP_ACCEPT check to differentiate between index and ajax call
@ -780,9 +782,9 @@ def package_models(request, package_uuid):
elif request.method == "POST": elif request.method == "POST":
log_post_params(request) log_post_params(request)
# Clean for potential XSS
name = request.POST.get("model-name") name = nh3.clean(request.POST.get("model-name"), tags=s.ALLOWED_HTML_TAGS).strip()
description = request.POST.get("model-description") description = nh3.clean(request.POST.get("model-description"), tags=s.ALLOWED_HTML_TAGS).strip()
model_type = request.POST.get("model-type") model_type = request.POST.get("model-type")
@ -864,7 +866,8 @@ def package_model(request, package_uuid, model_uuid):
ad_assessment = request.GET.get("app-domain-assessment", False) ad_assessment = request.GET.get("app-domain-assessment", False)
if classify or ad_assessment: if classify or ad_assessment:
smiles = request.GET.get("smiles", "").strip() # Clean for potential XSS
smiles = nh3.clean(request.GET.get("smiles", ""), tags=s.ALLOWED_HTML_TAGS).strip()
# Check if smiles is non empty and valid # Check if smiles is non empty and valid
if smiles == "": if smiles == "":
@ -927,8 +930,9 @@ def package_model(request, package_uuid, model_uuid):
else: else:
return HttpResponseBadRequest() return HttpResponseBadRequest()
else: else:
name = request.POST.get("model-name", "").strip() # Clean for potential XSS
description = request.POST.get("model-description", "").strip() name = nh3.clean(request.POST.get("model-name", "").strip(), tags=s.ALLOWED_HTML_TAGS).strip()
description = nh3.clean(request.POST.get("model-description", "").strip(), tags=s.ALLOWED_HTML_TAGS).strip()
if any([name, description]): if any([name, description]):
if name: if name:
@ -1030,9 +1034,9 @@ def package(request, package_uuid):
else: else:
return HttpResponseBadRequest() return HttpResponseBadRequest()
new_package_name = request.POST.get("package-name") # Clean for potential XSS
# Sanitize for potential XSS new_package_name = nh3.clean(request.POST.get("package-name"), tags=s.ALLOWED_HTML_TAGS).strip()
new_package_description = nh3.clean(request.POST.get("package-description"), tags=s.ALLOWED_HTML_TAGS) new_package_description = nh3.clean(request.POST.get("package-description"), tags=s.ALLOWED_HTML_TAGS).strip()
grantee_url = request.POST.get("grantee") grantee_url = request.POST.get("grantee")
read = request.POST.get("read") == "on" read = request.POST.get("read") == "on"
@ -1140,9 +1144,10 @@ def package_compounds(request, package_uuid):
return render(request, "collections/objects_list.html", context) return render(request, "collections/objects_list.html", context)
elif request.method == "POST": elif request.method == "POST":
compound_name = request.POST.get("compound-name") # Clean for potential XSS
compound_smiles = request.POST.get("compound-smiles") compound_name = nh3.clean(request.POST.get("compound-name"), tags=s.ALLOWED_HTML_TAGS).strip()
compound_description = request.POST.get("compound-description") compound_smiles = nh3.clean(request.POST.get("compound-smiles"), tags=s.ALLOWED_HTML_TAGS).strip()
compound_description = nh3.clean(request.POST.get("compound-description"), tags=s.ALLOWED_HTML_TAGS).strip()
c = Compound.create(current_package, compound_smiles, compound_name, compound_description) c = Compound.create(current_package, compound_smiles, compound_name, compound_description)
@ -1193,9 +1198,10 @@ def package_compound(request, package_uuid, compound_uuid):
return JsonResponse({"error": str(e)}, status=400) return JsonResponse({"error": str(e)}, status=400)
return JsonResponse({"success": current_compound.url}) return JsonResponse({"success": current_compound.url})
# Clean for potential XSS
new_compound_name = request.POST.get("compound-name", "").strip() new_compound_name = nh3.clean(request.POST.get("compound-name", ""), tags=s.ALLOWED_HTML_TAGS).strip()
new_compound_description = request.POST.get("compound-description", "").strip() new_compound_description = nh3.clean(request.POST.get("compound-description", ""),
tags=s.ALLOWED_HTML_TAGS).strip()
if new_compound_name: if new_compound_name:
current_compound.name = new_compound_name current_compound.name = new_compound_name
@ -1259,9 +1265,10 @@ def package_compound_structures(request, package_uuid, compound_uuid):
return render(request, "collections/objects_list.html", context) return render(request, "collections/objects_list.html", context)
elif request.method == "POST": elif request.method == "POST":
structure_name = request.POST.get("structure-name") # Clean for potential XSS
structure_smiles = request.POST.get("structure-smiles") structure_name = nh3.clean(request.POST.get("structure-name"), tags=s.ALLOWED_HTML_TAGS).strip()
structure_description = request.POST.get("structure-description") structure_smiles = nh3.clean(request.POST.get("structure-smiles"), tags=s.ALLOWED_HTML_TAGS).strip()
structure_description = nh3.clean(request.POST.get("structure-description"), tags=s.ALLOWED_HTML_TAGS).strip()
cs = current_compound.add_structure(structure_smiles, structure_name, structure_description) cs = current_compound.add_structure(structure_smiles, structure_name, structure_description)
@ -1321,9 +1328,11 @@ def package_compound_structure(request, package_uuid, compound_uuid, structure_u
return redirect(current_compound.url + "/structure") return redirect(current_compound.url + "/structure")
else: else:
return HttpResponseBadRequest() return HttpResponseBadRequest()
# Clean for potential XSS
new_structure_name = request.POST.get("compound-structure-name", "").strip() new_structure_name = nh3.clean(request.POST.get("compound-structure-name", ""),
new_structure_description = request.POST.get("compound-structure-description", "").strip() tags=s.ALLOWED_HTML_TAGS).strip()
new_structure_description = nh3.clean(request.POST.get("compound-structure-description", ""),
tags=s.ALLOWED_HTML_TAGS).strip()
if new_structure_name: if new_structure_name:
current_structure.name = new_structure_name current_structure.name = new_structure_name
@ -1416,8 +1425,9 @@ def package_rules(request, package_uuid):
log_post_params(request) log_post_params(request)
# Generic params # Generic params
rule_name = request.POST.get("rule-name") # Clean for potential XSS
rule_description = request.POST.get("rule-description") rule_name = nh3.clean(request.POST.get("rule-name"), tags=s.ALLOWED_HTML_TAGS).strip()
rule_description = nh3.clean(request.POST.get("rule-description"), tags=s.ALLOWED_HTML_TAGS).strip()
rule_type = request.POST.get("rule-type") rule_type = request.POST.get("rule-type")
@ -1425,11 +1435,16 @@ def package_rules(request, package_uuid):
# Obtain parameters as required by rule type # Obtain parameters as required by rule type
if rule_type == "SimpleAmbitRule": if rule_type == "SimpleAmbitRule":
params["smirks"] = request.POST.get("rule-smirks") # Clean for potential XSS
params["reactant_filter_smarts"] = request.POST.get("rule-reactant-smarts") params["smirks"] = nh3.clean(request.POST.get("rule-smirks"), tags=s.ALLOWED_HTML_TAGS).strip()
params["product_filter_smarts"] = request.POST.get("rule-product-smarts") params["reactant_filter_smarts"] = nh3.clean(request.POST.get("rule-reactant-smarts"),
tags=s.ALLOWED_HTML_TAGS).strip()
params["product_filter_smarts"] = nh3.clean(request.POST.get("rule-product-smarts"),
tags=s.ALLOWED_HTML_TAGS).strip()
elif rule_type == "SimpleRDKitRule": elif rule_type == "SimpleRDKitRule":
params["reaction_smarts"] = request.POST.get("rule-reaction-smarts") # Clean for potential XSS
params["reaction_smarts"] = nh3.clean(request.POST.get("rule-reaction-smarts"),
tags=s.ALLOWED_HTML_TAGS).strip()
elif rule_type == "ParallelRule": elif rule_type == "ParallelRule":
pass pass
elif rule_type == "SequentialRule": elif rule_type == "SequentialRule":
@ -1522,8 +1537,9 @@ def package_rule(request, package_uuid, rule_uuid):
return JsonResponse({"success": current_rule.url}) return JsonResponse({"success": current_rule.url})
rule_name = request.POST.get("rule-name", "").strip() # Clean for potential XSS
rule_description = request.POST.get("rule-description", "").strip() rule_name = nh3.clean(request.POST.get("rule-name", ""), tags=s.ALLOWED_HTML_TAGS).strip()
rule_description = nh3.clean(request.POST.get("rule-description", "").strip(), tags=s.ALLOWED_HTML_TAGS).strip()
if rule_name: if rule_name:
current_rule.name = rule_name current_rule.name = rule_name
@ -1584,9 +1600,10 @@ def package_reactions(request, package_uuid):
return render(request, "collections/objects_list.html", context) return render(request, "collections/objects_list.html", context)
elif request.method == "POST": elif request.method == "POST":
reaction_name = request.POST.get("reaction-name") # Clean for potential XSS
reaction_description = request.POST.get("reaction-description") reaction_name = nh3.clean(request.POST.get("reaction-name"), tags=s.ALLOWED_HTML_TAGS).strip()
reactions_smirks = request.POST.get("reaction-smirks") reaction_description = nh3.clean(request.POST.get("reaction-description"), tags=s.ALLOWED_HTML_TAGS).strip()
reactions_smirks = nh3.clean(request.POST.get("reaction-smirks"), tags=s.ALLOWED_HTML_TAGS).strip()
educts = reactions_smirks.split(">>")[0].split(".") educts = reactions_smirks.split(">>")[0].split(".")
products = reactions_smirks.split(">>")[1].split(".") products = reactions_smirks.split(">>")[1].split(".")
@ -1648,8 +1665,10 @@ def package_reaction(request, package_uuid, reaction_uuid):
return JsonResponse({"success": current_reaction.url}) return JsonResponse({"success": current_reaction.url})
new_reaction_name = request.POST.get("reaction-name", "").strip() # Clean for potential XSS
new_reaction_description = request.POST.get("reaction-description", "").strip() new_reaction_name = nh3.clean(request.POST.get("reaction-name", ""), tags=s.ALLOWED_HTML_TAGS).strip()
new_reaction_description = nh3.clean(request.POST.get("reaction-description", ""),
tags=s.ALLOWED_HTML_TAGS).strip()
if new_reaction_name: if new_reaction_name:
current_reaction.name = new_reaction_name current_reaction.name = new_reaction_name
@ -1724,11 +1743,12 @@ def package_pathways(request, package_uuid):
elif request.method == "POST": elif request.method == "POST":
log_post_params(request) log_post_params(request)
name = request.POST.get("name") # Clean for potential XSS
# Sanitize for potential XSS name = nh3.clean(request.POST.get("name"), tags=s.ALLOWED_HTML_TAGS).strip()
description = nh3.clean(request.POST.get("description"), tags=s.ALLOWED_HTML_TAGS) description = nh3.clean(request.POST.get("description"), tags=s.ALLOWED_HTML_TAGS).strip()
smiles = nh3.clean(request.POST.get("smiles", ""), tags=s.ALLOWED_HTML_TAGS).strip()
pw_mode = request.POST.get("predict", "predict").strip() pw_mode = request.POST.get("predict", "predict").strip()
smiles = request.POST.get("smiles", "").strip()
if "smiles" in request.POST and smiles == "": if "smiles" in request.POST and smiles == "":
return error( return error(
@ -1737,8 +1757,6 @@ def package_pathways(request, package_uuid):
"Pathway prediction failed due to missing or empty SMILES", "Pathway prediction failed due to missing or empty SMILES",
) )
smiles = smiles.strip()
try: try:
stand_smiles = FormatConverter.standardize(smiles) stand_smiles = FormatConverter.standardize(smiles)
except ValueError: except ValueError:
@ -1878,9 +1896,9 @@ def package_pathway(request, package_uuid, pathway_uuid):
return JsonResponse({"success": current_pathway.url}) return JsonResponse({"success": current_pathway.url})
pathway_name = request.POST.get("pathway-name") # Clean for potential XSS
# Sanitize for potential XSS pathway_name = nh3.clean(request.POST.get("pathway-name"), tags=s.ALLOWED_HTML_TAGS).strip()
pathway_description = nh3.clean(request.POST.get("pathway-description"), tags=s.ALLOWED_HTML_TAGS) pathway_description = nh3.clean(request.POST.get("pathway-description"), tags=s.ALLOWED_HTML_TAGS).strip()
if any([pathway_name, pathway_description]): if any([pathway_name, pathway_description]):
if pathway_name is not None and pathway_name.strip() != "": if pathway_name is not None and pathway_name.strip() != "":
@ -1960,9 +1978,10 @@ def package_pathway_nodes(request, package_uuid, pathway_uuid):
return render(request, "collections/objects_list.html", context) return render(request, "collections/objects_list.html", context)
elif request.method == "POST": elif request.method == "POST":
node_name = request.POST.get("node-name") # Clean for potential XSS
node_description = request.POST.get("node-description") node_name = nh3.clean(request.POST.get("node-name"), tags=s.ALLOWED_HTML_TAGS).strip()
node_smiles = request.POST.get("node-smiles") node_description = nh3.clean(request.POST.get("node-description"), tags=s.ALLOWED_HTML_TAGS).strip()
node_smiles = nh3.clean(request.POST.get("node-smiles"), tags=s.ALLOWED_HTML_TAGS).strip()
current_pathway.add_node(node_smiles, name=node_name, description=node_description) current_pathway.add_node(node_smiles, name=node_name, description=node_description)
@ -2092,9 +2111,10 @@ def package_pathway_edges(request, package_uuid, pathway_uuid):
elif request.method == "POST": elif request.method == "POST":
log_post_params(request) log_post_params(request)
# Clean for potential XSS
edge_name = nh3.clean(request.POST.get("edge-name"), tags=s.ALLOWED_HTML_TAGS).strip()
edge_description = nh3.clean(request.POST.get("edge-description"), tags=s.ALLOWED_HTML_TAGS).strip()
edge_name = request.POST.get("edge-name")
edge_description = request.POST.get("edge-description")
edge_substrates = request.POST.getlist("edge-substrates") edge_substrates = request.POST.getlist("edge-substrates")
edge_products = request.POST.getlist("edge-products") edge_products = request.POST.getlist("edge-products")
@ -2181,7 +2201,7 @@ def package_scenarios(request, package_uuid):
"all", False "all", False
): ):
scens = Scenario.objects.filter(package=current_package).order_by("name") scens = Scenario.objects.filter(package=current_package).order_by("name")
res = [{"name": s.name, "url": s.url, "uuid": s.uuid} for s in scens] res = [{"name": s_.name, "url": s_.url, "uuid": s_.uuid} for s_ in scens]
return JsonResponse(res, safe=False) return JsonResponse(res, safe=False)
context = get_base_context(request) context = get_base_context(request)
@ -2229,21 +2249,21 @@ def package_scenarios(request, package_uuid):
"name": "soil", "name": "soil",
"widgets": [ "widgets": [
HTMLGenerator.generate_html(ai, prefix=f"soil_{0}") HTMLGenerator.generate_html(ai, prefix=f"soil_{0}")
for ai in [x for s in SOIL_ADDITIONAL_INFORMATION.values() for x in s] for ai in [x for sv in SOIL_ADDITIONAL_INFORMATION.values() for x in sv]
], ],
}, },
"Sludge Data": { "Sludge Data": {
"name": "sludge", "name": "sludge",
"widgets": [ "widgets": [
HTMLGenerator.generate_html(ai, prefix=f"sludge_{0}") HTMLGenerator.generate_html(ai, prefix=f"sludge_{0}")
for ai in [x for s in SLUDGE_ADDITIONAL_INFORMATION.values() for x in s] for ai in [x for sv in SLUDGE_ADDITIONAL_INFORMATION.values() for x in sv]
], ],
}, },
"Water-Sediment System Data": { "Water-Sediment System Data": {
"name": "sediment", "name": "sediment",
"widgets": [ "widgets": [
HTMLGenerator.generate_html(ai, prefix=f"sediment_{0}") HTMLGenerator.generate_html(ai, prefix=f"sediment_{0}")
for ai in [x for s in SEDIMENT_ADDITIONAL_INFORMATION.values() for x in s] for ai in [x for sv in SEDIMENT_ADDITIONAL_INFORMATION.values() for x in sv]
], ],
}, },
} }
@ -2256,8 +2276,10 @@ def package_scenarios(request, package_uuid):
elif request.method == "POST": elif request.method == "POST":
log_post_params(request) log_post_params(request)
scenario_name = request.POST.get("scenario-name") # Clean for potential XSS
scenario_description = request.POST.get("scenario-description") scenario_name = nh3.clean(request.POST.get("scenario-name"), tags=s.ALLOWED_HTML_TAGS).strip()
scenario_description = nh3.clean(request.POST.get("scenario-description"), tags=s.ALLOWED_HTML_TAGS).strip()
scenario_date_year = request.POST.get("scenario-date-year") scenario_date_year = request.POST.get("scenario-date-year")
scenario_date_month = request.POST.get("scenario-date-month") scenario_date_month = request.POST.get("scenario-date-month")
scenario_date_day = request.POST.get("scenario-date-day") scenario_date_day = request.POST.get("scenario-date-day")
@ -2270,10 +2292,10 @@ def package_scenarios(request, package_uuid):
scenario_type = request.POST.get("scenario-type") scenario_type = request.POST.get("scenario-type")
additional_information = HTMLGenerator.build_models(request.POST.dict()) additional_information = HTMLGenerator.build_models(clean_dict(request.POST.dict()))
additional_information = [x for s in additional_information.values() for x in s] additional_information = [x for sv in additional_information.values() for x in sv]
s = Scenario.create( new_scen = Scenario.create(
current_package, current_package,
name=scenario_name, name=scenario_name,
description=scenario_description, description=scenario_description,
@ -2282,7 +2304,7 @@ def package_scenarios(request, package_uuid):
additional_information=additional_information, additional_information=additional_information,
) )
return redirect(s.url) return redirect(new_scen.url)
else: else:
return HttpResponseNotAllowed( return HttpResponseNotAllowed(
[ [
@ -2341,7 +2363,7 @@ def package_scenario(request, package_uuid, scenario_uuid):
current_scenario.save() current_scenario.save()
return redirect(current_scenario.url) return redirect(current_scenario.url)
elif hidden == "set-additional-information": elif hidden == "set-additional-information":
post_dict = sanitize_dict(request.POST.dict()) # Sanitise post dict inputs for potential XSS post_dict = clean_dict(request.POST.dict()) # Clean post dict inputs for potential XSS
ais = HTMLGenerator.build_models(post_dict) ais = HTMLGenerator.build_models(post_dict)
if s.DEBUG: if s.DEBUG:
@ -2350,7 +2372,7 @@ def package_scenario(request, package_uuid, scenario_uuid):
current_scenario.set_additional_information(ais) current_scenario.set_additional_information(ais)
return redirect(current_scenario.url) return redirect(current_scenario.url)
elif hidden == "add-additional-information": elif hidden == "add-additional-information":
post_dict = sanitize_dict(request.POST.dict()) # Sanitise post dict inputs for potential XSS post_dict = clean_dict(request.POST.dict()) # Clean post dict inputs for potential XSS
ais = HTMLGenerator.build_models(post_dict) ais = HTMLGenerator.build_models(post_dict)
if len(ais.keys()) != 1: if len(ais.keys()) != 1:
@ -2491,8 +2513,10 @@ def groups(request):
return render(request, "collections/objects_list.html", context) return render(request, "collections/objects_list.html", context)
elif request.method == "POST": elif request.method == "POST":
group_name = request.POST.get("group-name") # Clean for potential XSS
group_description = request.POST.get("group-description", s.DEFAULT_VALUES["description"]) group_name = nh3.clean(request.POST.get("group-name"), tags=s.ALLOWED_HTML_TAGS).strip()
group_description = nh3.clean(request.POST.get("group-description", s.DEFAULT_VALUES["description"]),
tags=s.ALLOWED_HTML_TAGS).strip()
g = GroupManager.create_group(current_user, group_name, group_description) g = GroupManager.create_group(current_user, group_name, group_description)
@ -2582,8 +2606,10 @@ def settings(request):
logger.info("Parameters received:") logger.info("Parameters received:")
logger.info(f"{k}\t{v}") logger.info(f"{k}\t{v}")
name = request.POST.get("prediction-setting-name") # Clean for potential XSS
description = request.POST.get("prediction-setting-description") name = nh3.clean(request.POST.get("prediction-setting-name"), tags=s.ALLOWED_HTML_TAGS).strip()
description = nh3.clean(request.POST.get("prediction-setting-description"), tags=s.ALLOWED_HTML_TAGS).strip()
new_default = request.POST.get("prediction-setting-new-default", "off") == "on" new_default = request.POST.get("prediction-setting-new-default", "off") == "on"
max_nodes = min( max_nodes = min(