Serious Form Security

Avatar of Chris Coyier
Der Chris Coyier am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Das Website Change Request Form ist hier schon seit einiger Zeit ein Dauerthema, und ich werde es noch eine Weile weiterverfolgen. Wir werden nicht das gesamte HTML und JavaScript, das das Formular zum Laufen bringt, wiederholen, also wenn Sie aufholen müssen, schauen Sie sich den ersten Artikel an.

Was wir bisher haben, ist ein ziemlich gut aussehendes Formular mit einer ziemlich guten Benutzererfahrung. Ich finde jedoch, dass ihm zwei wichtige Dinge fehlen. A) Die Benachrichtigungs-E-Mails selbst sind eher blasse und einfache Text-E-Mails und B) es gibt fast keinerlei Sicherheit im Formular selbst.

Dank Daniel Friedrich habe ich nun einige ernsthaftere Sicherheitsmaßnahmen in das Formular integriert, und das wird der Schwerpunkt dieses Artikels sein. Die beiden großen Ziele sind:

  • Das Formular wird von einem Menschen eingereicht
  • Dieser Mensch tut nichts Böswilliges

Token-Abgleich

Als erstes werden wir einen „Token“ generieren, im Wesentlichen einen geheimen Code. Dieser Token wird Teil unserer „Session“ sein, was bedeutet, dass er serverseitig gespeichert wird. Dieser Token wird **auch** als verstecktes Eingabefeld in das Formular selbst eingefügt, wenn es im Browser generiert wird. Das bedeutet, dass dieser Token **sowohl** auf der Client-Seite als auch auf der Server-Seite existiert und wir sie abgleichen können, wenn das Formular übermittelt wird, um sicherzustellen, dass sie übereinstimmen. Dies stellt sicher, dass jede Übermittlung des Formulars **unser Formular** ist und kein Skript von Drittanbietern, das von einem anderen Server aus auf uns einstürmt.

Zuerst müssen wir eine Session starten, dann eine Funktion zum Erstellen des Tokens erstellen, ihn der Session zuweisen und ihn für unsere Nutzung zurückgeben.

session_start();

function generateFormToken($form) {
    
       // generate a token from an unique value
    	$token = md5(uniqid(microtime(), true));  
    	
    	// Write the generated token to the session variable to check it against the hidden field when the form is sent
    	$_SESSION[$form.'_token'] = $token; 
    	
    	return $token;

}

Der einzigartige Wert hier stammt von einem md5-Hash der microtime-Funktion, aber Daniel sagt, es gibt eine Reihe anderer Verschlüsselungsmethoden (wie Salt-Werte...). Sobald wir diese Funktion haben, können wir sie kurz bevor wir das Formular im Markup erstellen, aufrufen, um den Wert zu generieren.

<?php
   // generate a new token for the $_SESSION superglobal and put them in a hidden field
   $newToken = generateFormToken('form1');   
?>

Dann fügen Sie den Token als verstecktes Eingabefeld in das Formular selbst ein

<input name="token" type="hidden" value="<?php echo $newToken; ?>">

Nun sind wir bereit, die Token-Werte beim Absenden des Formulars miteinander zu vergleichen. Dazu erstellen wir eine Funktion.

function verifyFormToken($form) {
  // check if a session is started and a token is transmitted, if not return an error
  if (!isset($_SESSION[$form.'_token'])) { 
    return false;
  }
	
  // check if the form is sent with token in it
  if (!isset($_POST['token'])) {
    return false;
  }
	
  // compare the tokens against each other if they are still the same
  if ($_SESSION[$form.'_token'] !== $_POST['token']) {
    return false;
  }
	
  return true;
}

Diese Funktion prüft, ob der Token an beiden erforderlichen Stellen vorhanden ist und ob sie übereinstimmen. Wenn alle diese drei Bedingungen erfüllt sind, gibt die Funktion true zurück, andernfalls false. Jetzt prüfen wir diesen Wert, bevor wir fortfahren. Die Grundstruktur ist:

if (verifyFormToken('form1')) {

   // ... more security testing
   // if pass, send email

} else {
   
   echo "Hack-Attempt detected. Got ya!.";
   writeLog('Formtoken');

}

Hack-Protokollierung

Beachten Sie, dass wir in der obigen Struktur eine Funktion namens writeLog() verwenden, die einen String entgegennimmt. Es gibt eine Vielzahl von Umständen, unter denen wir böswillige Absichten erkannt und gestoppt haben. In diesen Fällen rufen wir die Funktion writeLog() auf, um den Fehler zu unserer eigenen Referenz zu protokollieren, und beenden dann das Skript.

Diese Funktion versucht, in eine Textdatei auf dem Server zu schreiben (diese Datei benötigt entsprechende Dateiberechtigungen, Schreibzugriff für den Benutzer) oder, falls dies fehlschlägt, sendet sie Ihnen eine E-Mail.

function writeLog($where) {

	$ip = $_SERVER["REMOTE_ADDR"]; // Get the IP from superglobal
	$host = gethostbyaddr($ip);    // Try to locate the host of the attack
	$date = date("d M Y");
	
	// create a logging message with php heredoc syntax
	$logging = <<<LOG
		\n
		<< Start of Message >>
		There was a hacking attempt on your form. \n 
		Date of Attack: {$date}
		IP-Adress: {$ip} \n
		Host of Attacker: {$host}
		Point of Attack: {$where}
		<< End of Message >>
LOG;
        
        // open log file
		if($handle = fopen('hacklog.log', 'a')) {
		
			fputs($handle, $logging);  // write the Data to file
			fclose($handle);           // close the file
			
		} else {  // if first method is not working, for example because of wrong file permissions, email the data
		
        	$to = '[email protected]';  
        	$subject = 'HACK ATTEMPT';
        	$header = 'From: [email protected]';
        	if (mail($to, $subject, $logging, $header)) {
        		echo "Sent notice to admin.";
        	}

	}
}

Nichts POSTED, was wir nicht angefragt haben

Wenn Werte an uns übermittelt werden, die keine Namen von Eingabefeldern in unserem eigenen Formular haben, ist **definitiv** etwas Seltsames im Gange. Wir erstellen eine „Whitelist“ von akzeptablen POST-Namen und überprüfen dann jeden einzelnen.

// Building a whitelist array with keys which will send through the form, no others would be accepted later on
$whitelist = array('token','req-name','req-email','typeOfChange','urgency','URL-main','addURLS', 'curText', 'newText', 'save-stuff');

// Building an array with the $_POST-superglobal 
foreach ($_POST as $key=>$item) { 
  // Check if the value $key (fieldname from $_POST) can be found in the whitelisting array, if not, die with a short message to the hacker
  if (!in_array($key, $whitelist)) {
			
    writeLog('Unknown form fields');
    ie("Hack-Attempt detected. Please use only the fields in the form");
			
  }
}

Gültige URL

Die clientseitige Validierung dieses Formulars überwacht dies, sodass es im Voraus abgefangen werden sollte, aber natürlich sollten wir auch im Backend prüfen.

// Lets check the URL whether it's a real URL or not. if not, stop the script
if (!filter_var($_POST['URL-main'],FILTER_VALIDATE_URL)) {
   writeLog('URL Validation');
   die('Please insert a valid URL');
}

Bereinigung von Werten

An diesem Punkt haben wir alle unsere Sicherheitsprüfungen durchgeführt und fahren mit der Erstellung und dem Versand der E-Mail fort. Die Übernahme aller Eingaben genau so, wie sie eingegeben wurden, und deren Weiterleitung ist ein potenzielles Sicherheitsrisiko. Zum Beispiel könnte JavaScript in ein Textfeld eingegeben und dann per E-Mail versendet und möglicherweise ausgeführt werden, wenn die E-Mail geöffnet wird.

Für Felder wie „Name“, bei denen es keinen Grund gibt, spezielle Tags zu verwenden, werden wir sie mit strip_tags() komplett entfernen. Für Textbereiche, in denen es Anlass zur Verwendung einiger Tags geben kann, werden wir nur die htmlentities()-Funktion verwenden, um sie sicher zu konvertieren.

Beispiel

$message .= "Name: " . strip_tags($_POST['req-name']) . "\n";

$message .= "NEW Content: " . htmlentities($_POST['newText']) . "\n";

Mehr und bessere Bereinigung

Krinkle schreibt dazu:

Ursprünglich hast du strip_tags() für normale Felder und htmlentities() für Inhalte verwendet. Das ist in Ordnung, außer dass es bewährte Praxis ist, auch ENT_NOQUOTES und „UTF-8“ zu deklarieren, da sonst Zeichen wie der Akzent auf dem e („é“) zu Mist wie Å@ werden könnten. Und da die meisten Server automatisch Backslashes vor Eingaben hinzufügen, die über $_POST[] hereinkommen, ist es keine schlechte Sache, stripslashes vorsichtshalber auszuführen, sonst hättest du einen Backslash vor jedem einzelnen oder doppelten Anführungszeichen, das in das Formular eingegeben wurde, sobald es in der Mailbox ist.

function stripcleantohtml($s){
  // Restores the added slashes (ie.: " I\'m John " for security in output, and escapes them in htmlentities(ie.:  " etc.)
  // Also strips any <html> tags it may encounter
  // Use: Anything that shouldn't contain html (pretty much everything that is not a textarea)
  return htmlentities(trim(strip_tags(stripslashes($s)), ENT_NOQUOTES, "UTF-8"));
}

function cleantohtml($s){
  // Restores the added slashes (ie.: " I\'m John " for security in output, and escapes them in htmlentities(ie.:  " etc.)
  // It preserves any <html> tags in that they are encoded aswell (like <html>)
  // As an extra security, if people would try to inject tags that would become tags after stripping away bad characters,
  // we do still strip tags but only after htmlentities, so any genuine code examples will stay
  // Use: For input fields that may contain html, like a textarea
  return strip_tags(htmlentities(trim(stripslashes($s)), ENT_NOQUOTES, "UTF-8"));
}

Verwendung in diesem Beispiel

$message .= "Name: " . stripcleantohtml($_POST['req-name']) . "\n";

$message .= "NEW Content: " . cleantohtml($_POST['newText']) . "\n";

Warum nicht ein CAPTCHA verwenden?

CAPTCHAs tun ziemlich gute Arbeit, um Spam von Formularen fernzuhalten, aber wir machten uns hier mehr Sorgen um Hacking als um Spam. Außerdem, da dieses Formular für Leute ist, die wir wahrscheinlich MÖGEN und denen wir versuchEN zu HELFEN, werden wir sie nicht durch das nervige Hopsen durch einen CAPTCHA quälen. Wenn Sie jedoch interessiert sind, ist ein super-duper einfacher selbstgemachter Captcha, etwas wie „Was sind zehn minus fünf?“ in ein Textfeld zu fragen und dann nach den Werten „5“ und jeder Großbuchstabenkombination von „fünf“ zu suchen, und wenn es eine Übereinstimmung gibt, dann fortfahren, sonst nicht. Diese Version des Formulars hat bereits die Funktion writeLog(), die bereit zur Nutzung ist, und die Logik würde sich gut um die Zeilen 75 und 76 herum einfügen.

Wenn Sie einen etwas robusteren CAPTCHA wünschen, schauen Sie sich reCAPTCHA an, das ziemlich einfach zu bedienen ist, Menschen hilft und sehr zugänglich ist.

Sicherheits-Gurus?

Wenn wir hier über Sicherheit sprechen, gibt es tendenziell viele Meinungen darüber, wie die Dinge gemacht werden. Wenn Sie Ideen haben, teilen Sie sie bitte so konstruktiv wie möglich unten mit, und ich werde sie verdauen und sehen, wie wir uns im Laufe der Zeit verbessern können.

Demo anzeigen Dateien herunterladen