Add CSRF protection and rate limiting to contact form

Improve contact form styling with dedicated CSS file Move contact form
styles from base.css to separate file Add security measures to custom
directory with .htaccess Update honeypot field styling and
implementation
This commit is contained in:
Ruben 2025-11-04 22:33:14 +01:00
parent c013c2cde3
commit 7e44e7e132
4 changed files with 126 additions and 92 deletions

View file

@ -5,10 +5,25 @@ $formSuccess = false;
$formErrors = [];
$formData = ['name' => '', 'email' => '', 'message' => ''];
// Start session for CSRF token and rate limiting
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Generate CSRF token if not exists
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
// Process form submission
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['contact_form_submit'])) {
$formSubmitted = true;
// Security: CSRF Token Validation
if (!isset($_POST['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
$formErrors[] = 'Ugyldig sikkerhetskode. Vennligst prøv igjen.';
}
// Spam Prevention 1: Honeypot field (should be empty)
if (!empty($_POST['website'])) {
$formErrors[] = 'Spam detected.';
@ -32,10 +47,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['contact_form_submit']
}
// Spam Prevention 4: Rate limiting (session-based)
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$lastSubmitTime = isset($_SESSION['last_contact_submit']) ? $_SESSION['last_contact_submit'] : 0;
if (time() - $lastSubmitTime < 60) {
$formErrors[] = 'Vennligst vent litt før du sender inn igjen.';
@ -208,11 +219,14 @@ $currentTime = time();
<?php if (!$formSuccess): ?>
<form method="post" action="<?= htmlspecialchars($_SERVER['REQUEST_URI']) ?>" class="contact-form-inner">
<!-- Honeypot field (hidden from users, bots will fill it) -->
<div style="position: absolute; left: -5000px;" aria-hidden="true">
<div class="hp-field" aria-hidden="true">
<label for="website">Website</label>
<input type="text" id="website" name="website" tabindex="-1" autocomplete="off">
</div>
<!-- CSRF Token -->
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<!-- Time-based token -->
<input type="hidden" name="form_start_time" value="<?= $currentTime ?>">

View file

@ -0,0 +1,97 @@
/* Contact Form Styles */
/* Honeypot field - hidden from users but looks normal to bots */
.hp-field {
position: absolute;
left: -5000px;
}
/* Contact form section */
.contact-form {
margin-top: 2rem;
margin-bottom: 2rem;
}
/* Success message */
.form-success {
background-color: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
padding: 1rem;
border-radius: 0.25rem;
margin-bottom: 1rem;
}
/* Error messages */
.form-errors {
background-color: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
padding: 1rem;
border-radius: 0.25rem;
margin-bottom: 1rem;
}
.form-errors ul {
margin: 0.5rem 0 0 0;
padding-left: 1.5rem;
}
/* Form fields */
.contact-form-inner .form-group {
margin-bottom: 1.3rem;
}
.contact-form-inner .form-group label {
display: block;
font-weight: 600;
margin-bottom: 0.3rem;
color: var(--color-grey);
}
.contact-form-inner .form-group label .required {
color: #dc3545;
}
.contact-form-inner .form-group input[type="text"],
.contact-form-inner .form-group input[type="email"],
.contact-form-inner .form-group textarea {
width: 100%;
padding: 0.6rem;
border: 1px solid #ccc;
border-radius: 0.25rem;
font-family: inherit;
font-size: 1rem;
box-sizing: border-box;
}
.contact-form-inner .form-group input[type="text"]:focus,
.contact-form-inner .form-group input[type="email"]:focus,
.contact-form-inner .form-group textarea:focus {
outline: none;
border-color: var(--color-green);
box-shadow: 0 0 0 0.2rem rgba(0, 156, 128, 0.25);
}
.contact-form-inner .form-group textarea {
resize: vertical;
min-height: 150px;
}
.contact-form-inner .form-group small {
display: block;
margin-top: 0.3rem;
color: #6c757d;
font-size: 0.875rem;
}
.contact-form-inner .form-group button[type="submit"] {
cursor: pointer;
border: none;
font-size: 1rem;
font-family: inherit;
}
.contact-form-inner .form-group button[type="submit"]:hover {
cursor: pointer;
}

10
custom/.htaccess Normal file
View file

@ -0,0 +1,10 @@
# Deny access to all files in custom directory
# Only allow access through PHP includes
<Files "*">
Require all denied
</Files>
# Allow access to CSS and font files
<FilesMatch "\.(css|woff|woff2|ttf|eot|svg)$">
Require all granted
</FilesMatch>

View file

@ -185,93 +185,6 @@ main {
/* FOOTER */
/* CONTACT FORM */
.contact-form {
margin-top: 2rem;
margin-bottom: 2rem;
.form-success {
background-color: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
padding: 1rem;
border-radius: 0.25rem;
margin-bottom: 1rem;
}
.form-errors {
background-color: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
padding: 1rem;
border-radius: 0.25rem;
margin-bottom: 1rem;
ul {
margin: 0.5rem 0 0 0;
padding-left: 1.5rem;
}
}
.contact-form-inner {
.form-group {
margin-bottom: 1.3rem;
label {
display: block;
font-weight: 600;
margin-bottom: 0.3rem;
color: var(--color-grey);
.required {
color: #dc3545;
}
}
input[type="text"],
input[type="email"],
textarea {
width: 100%;
padding: 0.6rem;
border: 1px solid #ccc;
border-radius: 0.25rem;
font-family: inherit;
font-size: 1rem;
box-sizing: border-box;
&:focus {
outline: none;
border-color: var(--color-green);
box-shadow: 0 0 0 0.2rem rgba(0, 156, 128, 0.25);
}
}
textarea {
resize: vertical;
min-height: 150px;
}
small {
display: block;
margin-top: 0.3rem;
color: #6c757d;
font-size: 0.875rem;
}
button[type="submit"] {
cursor: pointer;
border: none;
font-size: 1rem;
font-family: inherit;
&:hover {
cursor: pointer;
}
}
}
}
}
footer {
color: var(--color-green-light);
a {