Skip to content

Commit ec3b725

Browse files
Security report mailing and small issue description fix (#3930)
* Security report mailing and small issue description fix * Security vuln fix and sponsors+crypto addition * Add pyzipper
1 parent e84e4e6 commit ec3b725

File tree

6 files changed

+261
-3
lines changed

6 files changed

+261
-3
lines changed

poetry.lock

+58-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ tld = "0.13"
6565
openai = "^1.66.3"
6666
async-timeout = "^4.0.3"
6767
python-dateutil = "^2.9.0.post0"
68+
pyzipper = "^0.3.6"
6869
tweepy = "^4.15.0"
6970

7071

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<meta name="description"
7+
content="Security Vulnerability Report for {{ clean_domain }} - Please review the attached report for details.">
8+
<meta name="keywords"
9+
content="security, vulnerability, report, website security, cyber threats">
10+
<title>Security Vulnerability Report</title>
11+
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css"
12+
rel="stylesheet">
13+
</head>
14+
<body class="font-sans text-gray-700 bg-gray-100 p-6">
15+
<div class="max-w-2xl mx-auto bg-white rounded-lg shadow-lg p-8">
16+
<h1 class="text-2xl font-bold text-gray-800 mb-4">Security Vulnerability Report for {{ clean_domain }}</h1>
17+
<p class="text-base text-gray-600 mb-4">A security vulnerability has been reported for your domain.</p>
18+
<p class="text-base text-gray-600 mb-4">
19+
The attached zip file contains screenshots and details of the vulnerability.
20+
<br>
21+
For security reasons, the zip file is password-protected.
22+
</p>
23+
<p class="text-base text-gray-600 mb-4">
24+
<strong class="text-gray-800">Password:</strong> {{ password }}
25+
</p>
26+
<p class="text-base text-gray-600 mb-4">Please review this report as soon as possible and take appropriate action.</p>
27+
<p class="text-sm text-gray-500 italic">This is an automated message from our security reporting system.</p>
28+
</div>
29+
</body>
30+
</html>

website/templates/report.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ <h2 class="text-xl font-semibold text-gray-900 truncate">{% trans "Bug Descripti
195195
</div>
196196
</div>
197197
<textarea id="markdownInput"
198-
name="markdown-content"
198+
name="markdown_description"
199199
class="w-full placeholder:text-base h-[300px] p-4 text-gray-700 border border-gray-200 rounded-lg "
200200
placeholder="Enter your markdown here..."></textarea>
201201
<div id="preview-area"

website/utils.py

+11
Original file line numberDiff line numberDiff line change
@@ -710,6 +710,17 @@ def get_page_votes(template_name):
710710
return upvotes, downvotes
711711

712712

713+
def validate_screenshot_hash(screenshot_hash):
714+
"""
715+
Validate that the screenshot_hash only contains alphanumeric characters,
716+
hyphens, or underscores.
717+
"""
718+
if not re.match(r"^[a-zA-Z0-9_-]+$", screenshot_hash):
719+
raise ValidationError(
720+
"Invalid screenshot hash. Only alphanumeric characters, hyphens, and underscores are allowed."
721+
)
722+
723+
713724
# Twitter namespace
714725
class twitter:
715726
@staticmethod

website/views/issue.py

+160-1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
is_valid_https_url,
8080
rebuild_safe_url,
8181
safe_redirect_request,
82+
validate_screenshot_hash,
8283
)
8384

8485
from .constants import GSOC25_PROJECTS
@@ -944,7 +945,6 @@ def create_issue(self, form):
944945
"report.html",
945946
{"form": self.get_form(), "captcha_form": CaptchaForm()},
946947
)
947-
948948
tokenauth = False
949949
obj = form.save(commit=False)
950950
report_anonymous = self.request.POST.get("report_anonymous", "off") == "on"
@@ -979,6 +979,165 @@ def create_issue(self, form):
979979
domain = Domain.objects.create(name=clean_domain, url=clean_domain)
980980
domain.save()
981981

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+
9821141
hunt = self.request.POST.get("hunt", None)
9831142
if hunt is not None and hunt != "None":
9841143
hunt = Hunt.objects.filter(id=hunt).first()

0 commit comments

Comments
 (0)