Blog / Tehnologija i razvoj / PHP sigurnost

Sigurna PHP kontakt forma
preko SMTP-a

Kontakt forma izgleda kao jednostavan dio web stranice, ali iza nje stoji nekoliko važnih tehničkih odluka: validacija, spam zaštita, CSRF token, sigurno SMTP slanje, konfiguracija tajni, logiranje grešaka i zaštita od zloupotrebe.

Sigurna PHP kontakt forma i SMTP slanje emaila

Ovaj vodič pokazuje kako razmišljati o sigurnoj PHP kontakt formi koja šalje poruke preko SMTP-a, umjesto oslanjanja na PHP funkciju mail(). Primjeri su namjerno praktični: cilj nije napraviti framework, nego stabilan obrazac koji se može prilagoditi custom poslovnim web stranicama.

Zašto SMTP? Funkcija mail() često ovisi o konfiguraciji hostinga, zna biti ugašena zbog zloupotrebe i ne daje dovoljnu kontrolu nad autentifikacijom. SMTP s provjerenim računom, TLS-om i jasnim logiranjem je predvidljiviji, sigurniji i lakši za dijagnostiku.

1. Što kontakt forma mora riješiti

Kontakt forma nije samo HTML forma i slanje emaila. U produkciji treba riješiti barem pet problema: korisnik mora moći poslati poruku, botovi ne smiju zatrpati inbox, podaci moraju biti validirani, mail mora proći kroz pouzdan kanal, a greške moraju biti vidljive developeru bez otkrivanja osjetljivih informacija korisniku.

Najčešća greška je tretirati kontakt formu kao “mali feature” koji se samo zalijepi na kraj. Ako forma ne radi, poslovna web stranica gubi upite. Ako forma radi nesigurno, može postati spam relay, izvor log smeća ili sigurnosni rizik.

Minimalni produkcijski standard

  • Server-side validacija svih polja, ne samo HTML5 validacija u browseru.
  • CSRF token za zaštitu od neželjenih cross-site submitova.
  • Honeypot polje ili drugi tihi anti-spam signal.
  • SMTP slanje preko autentificiranog računa i TLS/SSL zaštite.
  • Konfiguracija izvan javnog koda, bez hardcodanih lozinki u templateu.
  • Logiranje tehničkih grešaka bez prikaza SMTP detalja korisniku.

2. Predložena struktura datoteka

Kod manjih custom webova struktura može ostati jednostavna. Bitno je da se javni handler, konfiguracija i pomoćne klase ne miješaju u jednu nečitljivu datoteku. Ne treba uvoditi pretežak framework samo za kontakt formu, ali treba jasno razdvojiti odgovornosti.

Struktura Jednostavan layout za custom PHP web
project/
├── config.php
├── lib/
│   ├── mailer.php
│   └── validation.php
├── pages/
│   └── kontakt.php
├── public/
│   └── form-handler.php
└── vendor/
    └── phpmailer/

U stvarnom projektu putanje mogu biti drugačije, ali princip je isti: konfiguracija je centralizirana, PHPMailer setup je izdvojen, validacija je izdvojena, a handler samo orkestrira proces.

3. Konfiguracija: tajne ne smiju živjeti u HTML-u

SMTP lozinka, host, port i korisničko ime ne bi trebali biti razbacani po templateima. Idealno je koristiti environment varijable, ali na manjim hosting paketima često je realističnije imati produkcijski config izvan repozitorija ili barem config koji se ne commita s pravim tajnama.

PHP config Primjer SMTP konfiguracije
<?php

return [
    'smtp' => [
        'host' => 'mail.example.com',
        'port' => 587,
        'secure' => 'tls',
        'username' => 'website@example.com',
        'password' => 'use-a-real-secret-outside-git',
        'from_email' => 'website@example.com',
        'from_name' => 'Website kontakt forma',
        'to_email' => 'info@example.com',
    ],
];
Praktična napomena: ako je produkcijski config.php poseban za server, kod deploya ga ne treba prepisivati cijelog. Sigurnije je mijenjati samo ono što je stvarno potrebno, primjerice cache-busting assembly vrijednost, a SMTP tajne ostaviti netaknute.

4. HTML forma: jednostavna, ali s CSRF i honeypotom

Forma treba biti razumljiva korisniku, ali i dovoljno zaštićena od osnovnog bot prometa. Honeypot polje je skriveno polje koje normalan korisnik ne ispunjava, ali ga bot često popuni jer automatski šalje sva polja. CSRF token veže submit uz korisničku sesiju.

HTML/PHP Forma s CSRF tokenom i honeypotom
<?php
session_start();

$_SESSION['csrf_token'] ??= bin2hex(random_bytes(32));
?>

<form method="post" action="/form-handler.php" novalidate>
    <input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token'], ENT_QUOTES) ?>">

    <label>
        Ime
        <input type="text" name="name" autocomplete="name" required>
    </label>

    <label>
        Email
        <input type="email" name="email" autocomplete="email" required>
    </label>

    <label>
        Poruka
        <textarea name="message" rows="6" required></textarea>
    </label>

    <input type="text" name="website" tabindex="-1" autocomplete="off" class="visually-hidden">

    <button type="submit">Pošalji</button>
</form>

Client-side validacija je korisna za UX, ali nije sigurnosna mjera. Sve što dođe s forme mora se provjeriti na serveru jer bot ili napadač ne mora koristiti vaš HTML.

5. Server-side validacija

Validacija treba biti stroga, ali ne smije biti iritantna. Ime ne treba prihvatiti ako je prazno ili predugo. Email mora biti stvarna email adresa. Poruka treba imati minimalnu i maksimalnu duljinu. Honeypot mora biti prazan. CSRF token mora odgovarati tokenu u sesiji.

PHP Validacija inputa prije slanja maila
<?php

function clean_string(?string $value): string
{
    return trim((string) $value);
}

function validate_contact_input(array $post, array $session): array
{
    $errors = [];

    $name = clean_string($post['name'] ?? '');
    $email = clean_string($post['email'] ?? '');
    $message = clean_string($post['message'] ?? '');
    $csrf = clean_string($post['csrf_token'] ?? '');
    $honeypot = clean_string($post['website'] ?? '');

    if ($honeypot !== '') {
        $errors[] = 'Spam signal detected.';
    }

    if ($csrf === '' || !hash_equals($session['csrf_token'] ?? '', $csrf)) {
        $errors[] = 'Invalid form token.';
    }

    if ($name === '' || mb_strlen($name) > 120) {
        $errors[] = 'Name is required and must be shorter than 120 characters.';
    }

    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        $errors[] = 'A valid email address is required.';
    }

    if (mb_strlen($message) < 10 || mb_strlen($message) > 5000) {
        $errors[] = 'Message must be between 10 and 5000 characters.';
    }

    return [$errors, compact('name', 'email', 'message')];
}

6. PHPMailer SMTP setup

PHPMailer je praktičan jer rješava MIME poruke, header encoding, SMTP autentifikaciju, TLS/SSL i dio edge caseova koje ne želite ručno pisati. Najvažnije je ne koristiti korisnikov email kao stvarni sender. Korisnikov email stavite kao Reply-To, a From neka bude domena ili mailbox koji SMTP server smije slati.

PHPMailer Slanje preko SMTP-a
<?php

use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;

function send_contact_email(array $config, array $data): void
{
    $mail = new PHPMailer(true);

    $mail->isSMTP();
    $mail->Host = $config['host'];
    $mail->Port = (int) $config['port'];
    $mail->SMTPAuth = true;
    $mail->Username = $config['username'];
    $mail->Password = $config['password'];
    $mail->SMTPSecure = $config['secure'] === 'ssl'
        ? PHPMailer::ENCRYPTION_SMTPS
        : PHPMailer::ENCRYPTION_STARTTLS;

    $mail->CharSet = 'UTF-8';
    $mail->setFrom($config['from_email'], $config['from_name']);
    $mail->addAddress($config['to_email']);
    $mail->addReplyTo($data['email'], $data['name']);

    $mail->Subject = 'Nova poruka s kontakt forme';
    $mail->Body = sprintf(
        "Ime: %s\nEmail: %s\n\nPoruka:\n%s",
        $data['name'],
        $data['email'],
        $data['message']
    );

    $mail->send();
}
Zašto ne From = korisnikov email? Ako pošaljete email s From adresom korisnika, a šaljete preko vlastitog SMTP servera, rušite SPF/DMARC očekivanja i povećavate šansu da poruka završi kao spam. Korisnik ide u Reply-To, a From ostaje legitimni mailbox vaše domene.

7. Handler koji spaja validaciju i slanje

Handler treba biti kratak i jasan. Ne smije prikazati SMTP lozinku, stack trace ili detalje servera korisniku. Korisnik treba dobiti jednostavnu poruku, a developer treba dobiti log koji pomaže riješiti problem.

PHP handler Kontrolirani submit flow
<?php

session_start();

require __DIR__ . '/../vendor/autoload.php';
require __DIR__ . '/../lib/validation.php';
require __DIR__ . '/../lib/mailer.php';

$config = require __DIR__ . '/../config.php';

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    exit('Method not allowed');
}

[$errors, $data] = validate_contact_input($_POST, $_SESSION);

if ($errors !== []) {
    $_SESSION['form_error'] = 'Provjerite unesene podatke i pokušajte ponovno.';
    header('Location: /kontakt');
    exit;
}

try {
    send_contact_email($config['smtp'], $data);
    unset($_SESSION['csrf_token']);
    $_SESSION['form_success'] = 'Poruka je uspješno poslana.';
} catch (Throwable $e) {
    error_log('Contact form SMTP error: ' . $e->getMessage());
    $_SESSION['form_error'] = 'Poruku trenutno nije moguće poslati. Pokušajte ponovno kasnije.';
}

header('Location: /kontakt');
exit;

8. Rate limit bez velikog sustava

Ako ne koristite Cloudflare, reCAPTCHA ili vanjski anti-spam servis, možete dodati vrlo jednostavan session-based rate limit. To nije neprobojna zaštita, ali smanjuje slučajno floodanje i dio jednostavnih bot pokušaja.

PHP Jednostavan session rate limit
<?php

function can_submit_contact_form(array &$session, int $cooldownSeconds = 45): bool
{
    $now = time();
    $lastSubmit = (int) ($session['last_contact_submit'] ?? 0);

    if ($lastSubmit > 0 && ($now - $lastSubmit) < $cooldownSeconds) {
        return false;
    }

    $session['last_contact_submit'] = $now;
    return true;
}

Za ozbiljniji traffic bolji je IP-based rate limit, firewall pravila, Cloudflare Turnstile ili drugi anti-abuse sloj. Ali i ovakav mali limit je bolji od potpuno otvorenog submitanja.

9. Što logirati, a što ne

Logovi trebaju pomoći developeru, ali ne smiju postati sigurnosni problem. U logovima ne trebaju završiti SMTP lozinke, cijeli sadržaj poruke, osobni podaci bez potrebe ili detalji koji bi napadaču pomogli. Dovoljno je logirati tip greške, vrijeme, eventualno IP hash i osnovni kontekst.

Logging Sigurniji oblik log poruke
<?php

function log_contact_error(Throwable $e): void
{
    error_log(sprintf(
        'Contact form failed at %s: %s',
        date('c'),
        $e->getMessage()
    ));
}

10. Testiranje prije produkcije

Kontakt formu treba testirati kao mali backend feature. Provjerite uspješan submit, krivi email, praznu poruku, predugu poruku, popunjen honeypot, neispravan CSRF token, ugašen SMTP račun i krivu lozinku. Ako sve to radi predvidljivo, forma je puno bliže produkcijskoj stabilnosti.

Test checklist

  • Poruka stiže u inbox i Reply-To vodi na korisnikovu email adresu.
  • Forma ne šalje email kada je CSRF token pogrešan.
  • Honeypot submit se odbija bez slanja maila.
  • Korisniku se ne prikazuje SMTP greška ili lozinka.
  • Log sadrži dovoljno informacija za debug.
  • Forma i dalje radi ako hosting ugasi PHP mail().

Zaključak: kontakt forma je mali feature s velikom odgovornošću

Sigurna PHP kontakt forma ne mora biti komplicirana, ali mora biti disciplinirana. SMTP, PHPMailer, validacija, CSRF, honeypot i kontrolirano logiranje čine veliku razliku između forme koja “radi na testu” i forme koja stabilno radi u produkciji.

Ako radite custom web, ovakav pristup se dobro uklapa u širu tehničku arhitekturu: čisti backend, server-side rendering, jasna konfiguracija i minimalan broj nepotrebnih ovisnosti. Za širi kontekst pročitajte i PHP vs .NET za poslovne web stranice te razvoj web stranica po mjeri.

Custom web razvoj Održavanje web stranica