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:
parent
c013c2cde3
commit
7e44e7e132
4 changed files with 126 additions and 92 deletions
|
|
@ -5,10 +5,25 @@ $formSuccess = false;
|
||||||
$formErrors = [];
|
$formErrors = [];
|
||||||
$formData = ['name' => '', 'email' => '', 'message' => ''];
|
$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
|
// Process form submission
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['contact_form_submit'])) {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['contact_form_submit'])) {
|
||||||
$formSubmitted = true;
|
$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)
|
// Spam Prevention 1: Honeypot field (should be empty)
|
||||||
if (!empty($_POST['website'])) {
|
if (!empty($_POST['website'])) {
|
||||||
$formErrors[] = 'Spam detected.';
|
$formErrors[] = 'Spam detected.';
|
||||||
|
|
@ -32,10 +47,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['contact_form_submit']
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spam Prevention 4: Rate limiting (session-based)
|
// 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;
|
$lastSubmitTime = isset($_SESSION['last_contact_submit']) ? $_SESSION['last_contact_submit'] : 0;
|
||||||
if (time() - $lastSubmitTime < 60) {
|
if (time() - $lastSubmitTime < 60) {
|
||||||
$formErrors[] = 'Vennligst vent litt før du sender inn igjen.';
|
$formErrors[] = 'Vennligst vent litt før du sender inn igjen.';
|
||||||
|
|
@ -208,11 +219,14 @@ $currentTime = time();
|
||||||
<?php if (!$formSuccess): ?>
|
<?php if (!$formSuccess): ?>
|
||||||
<form method="post" action="<?= htmlspecialchars($_SERVER['REQUEST_URI']) ?>" class="contact-form-inner">
|
<form method="post" action="<?= htmlspecialchars($_SERVER['REQUEST_URI']) ?>" class="contact-form-inner">
|
||||||
<!-- Honeypot field (hidden from users, bots will fill it) -->
|
<!-- 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>
|
<label for="website">Website</label>
|
||||||
<input type="text" id="website" name="website" tabindex="-1" autocomplete="off">
|
<input type="text" id="website" name="website" tabindex="-1" autocomplete="off">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- CSRF Token -->
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||||
|
|
||||||
<!-- Time-based token -->
|
<!-- Time-based token -->
|
||||||
<input type="hidden" name="form_start_time" value="<?= $currentTime ?>">
|
<input type="hidden" name="form_start_time" value="<?= $currentTime ?>">
|
||||||
|
|
||||||
|
|
|
||||||
97
content/kontakt/styles.css
Normal file
97
content/kontakt/styles.css
Normal 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
10
custom/.htaccess
Normal 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>
|
||||||
|
|
@ -185,93 +185,6 @@ main {
|
||||||
|
|
||||||
/* FOOTER */
|
/* 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 {
|
footer {
|
||||||
color: var(--color-green-light);
|
color: var(--color-green-light);
|
||||||
a {
|
a {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue