Building a Secure Contact Form in Django: Handling Form Submissions and Preventing URL Injection

Introduction:

Hello everyone, Asif Khan here. Recently, I encountered a persistent issue with my website’s contact form. It seemed that malicious individuals were exploiting it to send links, possibly leading to harmful content. Determined to secure my platform and prevent such abuse, I delved into crafting a robust solution. Today, I’m excited to share with you the creation of a highly secure contact form for my website. Through diligent implementation of various security measures in Django, I’ve ensured that no one can slip harmful links through the cracks anymore. Let’s dive into the details of how I tackled this challenge and fortified my website against unwanted intrusions.

Step-by-Step Guide:

  1. Installation:First, ensure you have Django installed. You can install Django using pip:
  2. pip install django
  3. Create Django Project:Start by creating a new Django project. Open your terminal or command prompt and run:
  4. django-admin startproject myproject Replace myproject with your desired project name.
  5. Create Django App:Next, create a new Django app within your project. Navigate into your project directory and run:
  6. python manage.py startapp contact_form

Create Models , Forms, and Views:

Now, let’s create the necessary files for handling the contact form.

  • Models (models.py):
# contact_form/models.py
from django.db import models

class ContactMessage(models.Model):
    name = models.CharField(max_length=100)
    email = models.EmailField()
    subject = models.CharField(max_length=200)
    message = models.TextField()

    def __str__(self):
        return self.subject

Sure! Let’s break down the code in simpler terms:

  1. Django Imports: The line from django.db import models brings in a special toolkit called Django. Think of Django as a box of tools that helps us build websites more easily. The models inside Django are like blueprints for how we want to store information in our website’s database.
  2. ContactMessage Model: Imagine we’re building a digital mailbox to receive messages from visitors. ContactMessage is like the design plan for our mailbox. It tells Django how to organize the messages we receive.
  3. Fields:
    • name: This is where we store the name of the person who sends us a message. It’s like the label on a letter saying who it’s from.
    • email: This is where we keep the email address of the person sending the message. It’s like the return address on an envelope.
    • subject: This is where we store the subject or title of the message. It’s like the subject line of an email.
    • message: This is where we store the actual content of the message. It’s like the main body of an email.
  4. __str__ Method: This is a special method that helps Django understand how to represent each message in a way that’s easy for us to understand. In this case, it returns the subject of the message. So, when we look at our list of messages, we’ll see each one’s subject first.

So, in simple terms, this code creates a structure for our website to store messages from visitors. Each message will have a name, email, subject, and message content. This makes it easy for us to read, organize, and reply to messages on our website.

Forms (forms.py):

# contact_form/forms.py
from django import forms
from django.core.exceptions import ValidationError
from .models import ContactMessage

class ContactForm(forms.ModelForm):
    class Meta:
        model = ContactMessage
        fields = ['name', 'email', 'subject', 'message']

  1. Django Imports: The line from django import forms imports a set of tools from Django specifically designed to create and handle forms. Forms are like digital versions of paper forms that visitors can fill out on our website.
  2. ContactForm Class: Just like the ContactMessage class we discussed earlier, ContactForm is another blueprint. But this time, it’s a blueprint for how our contact form will look and work on our website.
  3. Meta Class: Inside ContactForm, there’s a special class called Meta. This class provides additional information about the form.
    • model = ContactMessage: This line tells Django that our form is based on the ContactMessage model we defined earlier. So, our form will have fields for name, email, subject, and message, just like the ContactMessage model.
    • fields = ['name', 'email', 'subject', 'message']: This line specifies which fields from the ContactMessage model should be included in the form. So, our form will have fields for the name, email, subject, and message.

So, when visitors come to our website and want to send us a message, they’ll see a form with fields for their name, email, subject, and message. When they fill out the form and submit it, Django will handle saving the information into our database using the structure defined in the ContactMessage model. This makes it easy for us to collect and manage messages from visitors.

Views (views.py):

from django.http import JsonResponse
from django.views.decorators.csrf import csrf_protect
from .forms import ContactForm
from django.shortcuts import render
from django.utils.html import escape
import re

@csrf_protect
def contact(request):
    if request.method == 'POST':
        form = ContactForm(request.POST)
        if form.is_valid():
            message = form.cleaned_data['message']
            # Regular expression to match URLs
            url_pattern = re.compile(r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+')
            if url_pattern.search(message):
                return JsonResponse({'message': 'Links are not allowed in the message.'}, status=400)
            
            # Save sanitized data
            contact_instance = form.save(commit=False)
            # You may sanitize the message here if needed
            contact_instance.save()
            
            message = 'Thank you for contacting us! We will get back to you as soon as possible.'
            return JsonResponse({'message': escape(message)})
        else:
            return JsonResponse({'message': 'Form submission failed. Please check the entered data.'}, status=400)

    form = ContactForm()
    return render(request, 'contact.html', {'form': form})
  1. Django Imports: This section imports various components from Django that we’ll need in our view function. These include JsonResponse for returning JSON responses, csrf_protect for protecting against CSRF attacks, ContactForm from our forms file, render for rendering HTML templates, and escape for escaping HTML entities.
  2. CSRF Protection: The @csrf_protect decorator helps protect against Cross-Site Request Forgery (CSRF) attacks. This ensures that the form submissions are coming from our own site and not from a malicious source.
  3. Contact View Function: This is the main view function that handles the contact page. When a user visits the contact page, this function gets called.
  4. Form Handling:
    • POST Request Handling: If the user submits the contact form (via POST request), the function checks if the submitted form data is valid.
    • Validation: If the form is valid, it checks if the message contains any URLs. If URLs are found, it returns a JSON response indicating that links are not allowed. Otherwise, it saves the form data to the database after sanitizing it.
    • Response: It then returns a JSON response with a success message.
    • Validation Failure: If the form is not valid, it returns a JSON response indicating that the form submission failed.
  5. GET Request Handling: If the request method is not POST (i.e., it’s a GET request), the function creates a new instance of the ContactForm and renders the contact.html template, passing the form object to the template for display.

In simpler terms, this code handles the submission of the contact form, validates the data, saves it to the database, and returns appropriate responses based on the outcome. It also renders the contact form page with an empty form for users to fill out.

Create Templates:

Create HTML templates for the base.html page inside the templates directory of your app.

  • Contact Form Template (base.html):
<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <!-- Bootstrap CSS link -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
    <title>Your Website Title</title>
</head>
<body>
    <div class="container">
        {% block content %}{% endblock %}
    </div>

    <!-- jQuery -->
    <script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
    <!-- Bootstrap JS link -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
    {% block scripts %}{% endblock %}
</body>
</html>

Create Templates:

Create HTML templates for the contact form html page inside the templates directory of your app.

  • Contact Form Template (contact.html):
{% extends 'base.html' %}

{% block content %}
  <div class="container mt-5">
    <div class="row justify-content-center">
      <div class="col-md-6">
        <div class="card">
          <div class="card-body">
            <div id="messageDiv" class="mb-3"></div>
            <h2 class="text-center mb-4">Contact Us</h2>
            <form id="contactForm" method="post" action="{% url 'contact' %}" class="needs-validation" novalidate>
              {% csrf_token %}
              <div class="form-group">
                <label for="id_name" class="sr-only">Name:</label>
                <input type="text" name="name" id="id_name" class="form-control" placeholder="Your Name" required>
                <div class="invalid-feedback">Please enter your name.</div>
              </div>
              <div class="form-group">
                <label for="id_email" class="sr-only">Email:</label>
                <input type="email" name="email" id="id_email" class="form-control" placeholder="Your Email" required>
                <div class="invalid-feedback">Please enter a valid email address.</div>
              </div>
              <div class="form-group">
                <label for="id_subject" class="sr-only">Subject:</label>
                <input type="text" name="subject" id="id_subject" class="form-control" placeholder="Subject" required>
                <div class="invalid-feedback">Please enter a subject.</div>
              </div>
              <div class="form-group">
                <label for="id_message" class="sr-only">Message:</label>
                <textarea name="message" id="id_message" class="form-control" placeholder="Your Message" required></textarea>
                <div class="invalid-feedback">Please enter your message.</div>
              </div>
              <button type="button" id="submitBtn" class="btn btn-primary btn-block">
                <span class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
                Submit
              </button>
            </form>
          </div>
        </div>
      </div>
    </div>
  </div>

  <script>
    document.getElementById('submitBtn').addEventListener('click', function () {
      var form = document.getElementById('contactForm');
      
      // Add 'was-validated' class to trigger custom validation styles
      form.classList.add('was-validated');
  
      if (form.checkValidity()) {
        // Show loading spinner
        document.getElementById('submitBtn').disabled = true;
        document.getElementById('submitBtn').querySelector('span').classList.remove('d-none');
  
        // AJAX request
        var formData = new FormData(form);
        var xhr = new XMLHttpRequest();
  
        xhr.open('POST', '{% url 'contact' %}', true);
        xhr.onload = function () {
          if (xhr.status >= 200 && xhr.status < 300) {
            var data = JSON.parse(xhr.responseText);
  
            // Handle success
            var successDiv = document.getElementById('messageDiv');
            successDiv.innerHTML = '<div class="alert alert-success">' + data.message + '</div>';
  
            // Clear success message after 5 seconds
            clearMessage();
          } else {
            var errorData = JSON.parse(xhr.responseText);
  
            // Handle error
            document.getElementById('messageDiv').innerHTML = '<div class="alert alert-danger">' + errorData.message + '</div>';
  
            // Clear error message after 5 seconds
            clearMessage();
          }
  
          // Hide loading spinner
          document.getElementById('submitBtn').disabled = false;
          document.getElementById('submitBtn').querySelector('span').classList.add('d-none');
        };
  
        xhr.onerror = function () {
          // Handle network error
          document.getElementById('messageDiv').innerHTML = '<div class="alert alert-danger">An error occurred. Please try again later.</div>';
  
          // Clear error message after 5 seconds
          clearMessage();
  
          // Hide loading spinner
          document.getElementById('submitBtn').disabled = false;
          document.getElementById('submitBtn').querySelector('span').classList.add('d-none');
        };
  
        xhr.send(formData);
      }
    });
  
    function clearMessage() {
      // Clear message after 5 seconds
      setTimeout(function() {
        var messageDiv = document.getElementById('messageDiv');
        if (messageDiv) {
          messageDiv.innerHTML = '';
        }
      }, 5000);
    }
  </script>
  
{% endblock %}
  1. Template Inheritance: {% extends 'base.html' %} – This line tells Django to use the template named 'base.html' as the base layout for this template. It means this template will inherit styles and structure from 'base.html'.
  2. Content Block: {% block content %} ... {% endblock %} – This defines a block named content within the template. It’s a placeholder where content specific to this template can be inserted. In this case, it contains the HTML structure for the contact form.
  3. HTML Structure: Inside the content block, there’s a container with a row and column layout, containing a card. This card represents the contact form.
  4. Form Submission: The form has id="contactForm" and is set to submit to the URL named 'contact' (resolved by Django’s url template tag).
  5. AJAX Handling: When the submit button (id="submitBtn") is clicked, an AJAX request is made to the 'contact' URL. This request sends the form data and expects a JSON response. Based on the response, it updates the message displayed on the page.
  6. JavaScript Validation: JavaScript is used for form validation and handling form submission asynchronously. It checks if the form is valid before submitting it and displays appropriate messages to the user.

Configure URLs:

Finally, configure URLs to route requests to the appropriate views.

  • Project URLs (urls.py):
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('contact_form.urls')),
]
  1. Admin URL: path('admin/', admin.site.urls) – This line defines a URL pattern that directs requests starting with /admin/ to the Django admin interface. When a user visits /admin/, Django will display the admin interface where administrators can manage the site’s data.
  2. App URL: path('', include('app.urls')) – This line defines a URL pattern that includes URLs from another application named 'app'. The include() function allows us to include URL patterns from other URL configurations. In this case, it includes URLs defined in the urls.py file of the 'app' application.
    • '': This empty string signifies the root URL. When a user visits the root URL of the website (e.g., https://example.com/), Django will include the URLs defined in the 'app.urls' module. This is typically where the main views and functionality of the website are defined.
    • 'app.urls': This is the module containing the URL patterns for the 'app' application. By including these URLs, we can organize the URL configuration of our project into smaller, modular components, making it easier to manage and maintain.

Overall, this Django URLs configuration maps specific URL patterns to different parts of our Django project: the admin interface and the views and functionality defined in the 'app' application. It’s the backbone of how Django routes incoming requests to the appropriate views or handlers.

App URLs (urls.py inside contact_form directory):

from django.urls import path
from . import views

urlpatterns = [
    path('', views.contact, name='contact'),
]

Congratulations! You’ve successfully built a secure contact form in Django. By following these steps, you’ve implemented various security measures to prevent URL injection and ensure the integrity of user-submitted data.

Leave a Comment

Your email address will not be published. Required fields are marked *

Shopping Basket