Skip to content

Commit

Permalink
chg: [security] Added new domain name validator used in the file test…
Browse files Browse the repository at this point in the history
…ing/helpers.py (GHSA-9fhc-f3mr-w6h6).
  • Loading branch information
cedricbonhomme committed Nov 20, 2023
1 parent 49d94e6 commit 7b3e7ca
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 68 deletions.
12 changes: 11 additions & 1 deletion api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from authentication.models import User
from automation.models import HttpAutomatedTest, PingAutomatedTest
from testing.models import TlsScanHistory
from testing.validators import domain_name

#
# Model: User
Expand Down Expand Up @@ -118,12 +119,21 @@ class Meta:

class DomainNameSerializer(serializers.Serializer):
domain_name = serializers.CharField(
max_length=200, required=True, help_text="Domain name."
max_length=200,
required=True,
help_text="Domain name.",
validators=[domain_name],
)

class Meta:
fields = ["domain_name"]

def validate_domain_name(self, value):
"""
Check that value is a valid domain name.
"""
return domain_name(value)


class DomainNameAndServiceSerializer(serializers.Serializer):
domain_name = serializers.CharField(
Expand Down
36 changes: 0 additions & 36 deletions api/validators.py

This file was deleted.

1 change: 1 addition & 0 deletions api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@ def create(self, request, *args, **kwargs):
Checks the presence of a SOA record.
"""
domain_name = request.data.get("domain_name", None)
DomainNameSerializer(domain_name)
result = check_soa_record(domain_name)
return Response(result, status=status.HTTP_200_OK)

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "testing-platform"
version = "0.1.0"
version = "0.1.1"
description = "NC3 Testing Platform"
authors = [
"Romain Kieffer <romain.kieffer@nc3.lu>",
Expand Down
60 changes: 30 additions & 30 deletions testing/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,45 +20,21 @@
from weasyprint import CSS, HTML

from testing.models import TlsScanHistory
from testing.validators import domain_name, full_domain_validator

from .cipher_scoring import load_cipher_info

logger = logging.getLogger(__name__)


def full_domain_validator(hostname):
"""
Fully validates a domain name as compilant with the standard rules:
- Composed of series of labels concatenated with dots, as are all domain names.
- Each label must be between 1 and 63 characters long.
- The entire hostname (including the delimiting dots) has a maximum of 255 characters.
- Only characters 'a' through 'z' (in a case-insensitive manner), the digits '0' through '9'.
- Labels can't start or end with a hyphen.
"""
HOSTNAME_LABEL_PATTERN = re.compile(r"(?!-)[A-Z\d-]+(?<!-)$", re.IGNORECASE)
if not hostname:
return
if len(hostname) > 255:
raise Exception(
"The domain name cannot be composed of more than 255 characters."
)
if hostname[-1:] == ".":
hostname = hostname[:-1] # strip exactly one dot from the right, if present
for label in hostname.split("."):
if len(label) > 63:
raise Exception(
"The label '%(label)s' is too long (maximum is 63 characters)."
% {"label": label}
)
if not HOSTNAME_LABEL_PATTERN.match(label):
raise Exception(f"Unallowed characters in label '{label}'.")
return hostname


def get_http_report(target, rescan):
################################
# HTTP SCAN Mozilla Observatory
################################
try:
domain_name(target)
except Exception:
return {"error": "You entered an invalid hostname!"}
response = {}

logger.info(f"http scan: scanning {target}, with rescan set to {rescan}")
Expand Down Expand Up @@ -220,8 +196,12 @@ def get_tls_report(target, rescan):
return fetch_tls


def check_soa_record(target: str) -> bool:
def check_soa_record(target: str) -> Union[bool, Dict]:
"""Checks the presence of a SOA record for the Email Systems Testing."""
try:
domain_name(target)
except Exception:
return {"status": False, "statusmessage": "The given domain is invalid!"}
result = False
try:
answers = dns.resolver.query(target, "SOA")
Expand All @@ -235,6 +215,10 @@ def email_check(target: str) -> Dict[str, Any]:
"""Parses and validates MX, SPF, and DMARC records,
Checks for DNSSEC deployment, Checks for STARTTLS and TLS support.
Checks for the validity of the DKIM public key."""
try:
domain_name(target)
except Exception:
return {"status": False, "statusmessage": "The given domain is invalid!"}
result = {}
env = os.environ.copy()
cmd = [
Expand Down Expand Up @@ -498,6 +482,10 @@ def ipv6_check(


def web_server_check(domain: str):
try:
domain_name(domain)
except Exception:
return {"status": False, "statusmessage": "The given domain is invalid!"}
nmap = nmap3.Nmap()
logger.info(f"server scan: testing {domain}")
service_scans = nmap.nmap_version_detection(
Expand Down Expand Up @@ -540,6 +528,10 @@ def web_server_check(domain: str):


def web_server_check_no_raw_socket(hostname):
try:
domain_name(hostname)
except Exception:
return {"status": False, "statusmessage": "The given domain is invalid!"}
api_endpoint = "https://vulners.com/api/v3/burp/software/"
header = {
"User-Agent": "Vulners NMAP Plugin 1.7",
Expand Down Expand Up @@ -597,6 +589,10 @@ def tls_version_check(domain: str, service):
"""
Checks the version of TLS.
"""
try:
domain_name(domain)
except Exception:
return {"status": False, "statusmessage": "The given domain is invalid!"}
nmap = nmap3.Nmap()
logger.info(f"tls scan: Scanning host/domain {domain}")
tls_scans = nmap.nmap_version_detection(domain, args="--script ssl-enum-ciphers")
Expand Down Expand Up @@ -660,6 +656,10 @@ def tls_version_check(domain: str, service):
def check_dkim_public_key(domain: str, selectors: list):
"""Looks for a DKIM public key in a DNS field and verifies that it can be used to
encrypt data."""
try:
domain_name(domain)
except Exception:
return {"status": False, "statusmessage": "The given domain is invalid!"}
if len(selectors) == 0:
# TODO Check to get proper selector or have a database of selectors
selectors = [
Expand Down
52 changes: 52 additions & 0 deletions testing/validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import re

from rest_framework.serializers import ValidationError

pattern = re.compile(
r"^(([a-zA-Z]{1})|([a-zA-Z]{1}[a-zA-Z]{1})|"
r"([a-zA-Z]{1}[0-9]{1})|([0-9]{1}[a-zA-Z]{1})|"
r"([a-zA-Z0-9][-_.a-zA-Z0-9]{0,61}[a-zA-Z0-9]))\."
r"([a-zA-Z]{2,13}|[a-zA-Z0-9-]{2,30}.[a-zA-Z]{2,3})$"
)


def domain_name(value):
"""
Return whether or not given value is a valid domain.
See:
https://validators.readthedocs.io/en/latest/_modules/validators/domain.html#domain
"""
res = pattern.match(value)
if res:
return True
else:
raise ValidationError("This field must be a domain name.")


def full_domain_validator(hostname):
"""
Fully validates a domain name as compilant with the standard rules:
- Composed of series of labels concatenated with dots, as are all domain names.
- Each label must be between 1 and 63 characters long.
- The entire hostname (including the delimiting dots) has a maximum of 255 characters.
- Only characters 'a' through 'z' (in a case-insensitive manner), the digits '0' through '9'.
- Labels can't start or end with a hyphen.
"""
HOSTNAME_LABEL_PATTERN = re.compile(r"(?!-)[A-Z\d-]+(?<!-)$", re.IGNORECASE)
if not hostname:
return
if len(hostname) > 255:
raise Exception(
"The domain name cannot be composed of more than 255 characters."
)
if hostname[-1:] == ".":
hostname = hostname[:-1] # strip exactly one dot from the right, if present
for label in hostname.split("."):
if len(label) > 63:
raise Exception(
"The label '%(label)s' is too long (maximum is 63 characters)."
% {"label": label}
)
if not HOSTNAME_LABEL_PATTERN.match(label):
raise Exception(f"Unallowed characters in label '{label}'.")
return hostname

0 comments on commit 7b3e7ca

Please sign in to comment.