|
79 | 79 | is_valid_https_url,
|
80 | 80 | rebuild_safe_url,
|
81 | 81 | safe_redirect_request,
|
| 82 | + validate_screenshot_hash, |
82 | 83 | )
|
83 | 84 |
|
84 | 85 | from .constants import GSOC25_PROJECTS
|
@@ -944,7 +945,6 @@ def create_issue(self, form):
|
944 | 945 | "report.html",
|
945 | 946 | {"form": self.get_form(), "captcha_form": CaptchaForm()},
|
946 | 947 | )
|
947 |
| - |
948 | 948 | tokenauth = False
|
949 | 949 | obj = form.save(commit=False)
|
950 | 950 | report_anonymous = self.request.POST.get("report_anonymous", "off") == "on"
|
@@ -979,6 +979,165 @@ def create_issue(self, form):
|
979 | 979 | domain = Domain.objects.create(name=clean_domain, url=clean_domain)
|
980 | 980 | domain.save()
|
981 | 981 |
|
| 982 | + # Don't save issue if security vulnerability |
| 983 | + if form.instance.label == "4" or form.instance.label == 4: |
| 984 | + dest_email = getattr(domain, "email", None) |
| 985 | + if not dest_email and domain.organization: |
| 986 | + dest_email = getattr(domain.organization, "email", None) |
| 987 | + |
| 988 | + if dest_email: |
| 989 | + import logging |
| 990 | + import secrets |
| 991 | + import string |
| 992 | + import tempfile |
| 993 | + from pathlib import Path |
| 994 | + |
| 995 | + import pyzipper |
| 996 | + from django.core.exceptions import ValidationError |
| 997 | + from django.core.mail import EmailMessage |
| 998 | + |
| 999 | + logger = logging.getLogger(__name__) |
| 1000 | + |
| 1001 | + try: |
| 1002 | + with tempfile.TemporaryDirectory() as temp_dir: |
| 1003 | + password = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(11)) |
| 1004 | + zip_path = os.path.join(temp_dir, "security_report.zip") |
| 1005 | + |
| 1006 | + screenshot_paths = [] |
| 1007 | + |
| 1008 | + if self.request.FILES.getlist("screenshots"): |
| 1009 | + for idx, screenshot in enumerate(self.request.FILES.getlist("screenshots")): |
| 1010 | + file_path = os.path.join( |
| 1011 | + temp_dir, f"screenshot_{idx+1}{Path(screenshot.name).suffix}" |
| 1012 | + ) |
| 1013 | + with open(file_path, "wb+") as destination: |
| 1014 | + for chunk in screenshot.chunks(): |
| 1015 | + destination.write(chunk) |
| 1016 | + screenshot_paths.append(file_path) |
| 1017 | + |
| 1018 | + elif self.request.POST.get("screenshot-hash"): |
| 1019 | + screenshot_hashes = self.request.POST.get("screenshot-hash").split(",") |
| 1020 | + |
| 1021 | + for idx, screenshot_hash in enumerate(screenshot_hashes): |
| 1022 | + try: |
| 1023 | + validate_screenshot_hash(screenshot_hash.strip()) |
| 1024 | + except ValidationError as e: |
| 1025 | + messages.error(self.request, str(e)) |
| 1026 | + return HttpResponseRedirect("/") |
| 1027 | + |
| 1028 | + orig_path = os.path.join( |
| 1029 | + settings.MEDIA_ROOT, "uploads", f"{screenshot_hash.strip()}.png" |
| 1030 | + ) |
| 1031 | + |
| 1032 | + if not orig_path.startswith(os.path.abspath(settings.MEDIA_ROOT)): |
| 1033 | + messages.error(self.request, f"Invalid screenshot hash: {screenshot_hash}.") |
| 1034 | + return HttpResponseRedirect("/") |
| 1035 | + |
| 1036 | + if os.path.exists(orig_path): |
| 1037 | + dest_path = os.path.join(temp_dir, f"screenshot_{idx+1}.png") |
| 1038 | + import shutil |
| 1039 | + |
| 1040 | + shutil.copy(orig_path, dest_path) |
| 1041 | + screenshot_paths.append(dest_path) |
| 1042 | + |
| 1043 | + details_md_path = os.path.join(temp_dir, "vulnerability_details.md") |
| 1044 | + with open(details_md_path, "w", encoding="utf-8") as f: |
| 1045 | + f.write("# Security Vulnerability Report\n\n") |
| 1046 | + f.write(f"**URL:** {obj.url}\n") |
| 1047 | + f.write(f"**Domain:** {clean_domain}\n") |
| 1048 | + |
| 1049 | + if obj.cve_id: |
| 1050 | + f.write(f"**CVE ID:** {obj.cve_id}\n") |
| 1051 | + |
| 1052 | + f.write("\n**Description:**\n") |
| 1053 | + f.write(f"{obj.description}\n\n") |
| 1054 | + |
| 1055 | + if obj.markdown_description: |
| 1056 | + f.write("## Detailed Description\n") |
| 1057 | + f.write(f"{obj.markdown_description}\n\n") |
| 1058 | + |
| 1059 | + if ( |
| 1060 | + self.request.user.is_authenticated |
| 1061 | + and self.request.POST.get("report_anonymous", "off") != "on" |
| 1062 | + ): |
| 1063 | + username = self.request.user.username or "Unknown User" |
| 1064 | + email = self.request.user.email if self.request.user.email else "No email provided" |
| 1065 | + |
| 1066 | + f.write("### Reported by:\n") |
| 1067 | + f.write(f"- **Name:** {username}\n") |
| 1068 | + f.write(f"- **Email:** {email}\n") |
| 1069 | + |
| 1070 | + user_profile = getattr(self.request.user, "userprofile", None) |
| 1071 | + if user_profile: |
| 1072 | + if user_profile.github_url: |
| 1073 | + github_username = user_profile.github_url.rstrip("/").split("/")[-1] |
| 1074 | + sponsors_url = f"https://github.com/sponsors/{github_username}" |
| 1075 | + f.write( |
| 1076 | + f"- **💖 GitHub Sponsors:** [Sponsor]({sponsors_url}) (or [Profile]({user_profile.github_url}))\n" |
| 1077 | + ) |
| 1078 | + if user_profile.btc_address: |
| 1079 | + f.write(f"- **🟠 BTC Address:** {user_profile.btc_address}\n") |
| 1080 | + if user_profile.bch_address: |
| 1081 | + f.write(f"- **💚 BCH Address:** {user_profile.bch_address}\n") |
| 1082 | + if user_profile.eth_address: |
| 1083 | + f.write(f"- **💎 ETH Address:** {user_profile.eth_address}\n") |
| 1084 | + |
| 1085 | + f.write(f"\n**Report Date:** {timezone.now().strftime('%Y-%m-%d %H:%M:%S')}\n") |
| 1086 | + |
| 1087 | + screenshot_paths.append(details_md_path) |
| 1088 | + |
| 1089 | + with pyzipper.AESZipFile( |
| 1090 | + zip_path, "w", compression=pyzipper.ZIP_DEFLATED, encryption=pyzipper.WZ_AES |
| 1091 | + ) as zipf: |
| 1092 | + zipf.setpassword(password.encode()) |
| 1093 | + for file in screenshot_paths: |
| 1094 | + zipf.write(file, arcname=os.path.basename(file)) |
| 1095 | + |
| 1096 | + email_subject = f"Security Vulnerability Report for {clean_domain}" |
| 1097 | + html_body = render_to_string( |
| 1098 | + "email/security_report.html", |
| 1099 | + {"clean_domain": clean_domain, "password": password}, |
| 1100 | + ) |
| 1101 | + |
| 1102 | + try: |
| 1103 | + email = EmailMessage( |
| 1104 | + subject=email_subject, |
| 1105 | + body=html_body, |
| 1106 | + from_email=settings.DEFAULT_FROM_EMAIL, |
| 1107 | + to=[dest_email], |
| 1108 | + ) |
| 1109 | + email.content_subtype = "html" |
| 1110 | + |
| 1111 | + with open(zip_path, "rb") as f: |
| 1112 | + email.attach("security_report.zip", f.read(), "application/zip") |
| 1113 | + |
| 1114 | + email.send(fail_silently=False) |
| 1115 | + |
| 1116 | + messages.success( |
| 1117 | + self.request, |
| 1118 | + "Security vulnerability report sent securely to the organization. Thank you for your report.", |
| 1119 | + ) |
| 1120 | + return HttpResponseRedirect("/") |
| 1121 | + except Exception as e: |
| 1122 | + logger.error(f"Error while sending email: {e}") |
| 1123 | + messages.error( |
| 1124 | + self.request, |
| 1125 | + "Could not mail security report. Please try again later.", |
| 1126 | + ) |
| 1127 | + return HttpResponseRedirect("/") |
| 1128 | + |
| 1129 | + except Exception as e: |
| 1130 | + logger.error(f"Unexpected error: {e}") |
| 1131 | + messages.error(self.request, "An unexpected error occurred while processing the report.") |
| 1132 | + return HttpResponseRedirect("/") |
| 1133 | + |
| 1134 | + else: |
| 1135 | + messages.warning( |
| 1136 | + self.request, |
| 1137 | + "Could not send security vulnerability report as no contact email is available for this domain.", |
| 1138 | + ) |
| 1139 | + return HttpResponseRedirect("/") |
| 1140 | + |
982 | 1141 | hunt = self.request.POST.get("hunt", None)
|
983 | 1142 | if hunt is not None and hunt != "None":
|
984 | 1143 | hunt = Hunt.objects.filter(id=hunt).first()
|
|
0 commit comments