Hallo!

Kunnen jullie mij helpen mijn login systeem zo veilig mogelijk te maken? Hieronder staan mijn codes voor het login script. Alvast bedankt!

index.php

<?php
session_start(); // Start de session \\

// IMPORTEER ALLE BESTANDEN \\
REQUIRE_ONCE 'paneel/include/config.php';
REQUIRE_ONCE 'paneel/include/functions.php';
REQUIRE_ONCE 'paneel/include/login_script.php';

// ACTIVEER DE FUNCTIE 'LOGGED_IN' \\
if(logged_in()) {
	header('Location: /dashboard'); // Hier wordt je doorgelinkt naar '/dashboard' als je bent ingelogd \\
	die(); // Als je bent ingelogd, volgt de volgende code niet \\
}
// ONDERHOUD INSTELLINGEN \\
$currentLocation = "index"; // PAS DE LOCATIE NIET AAN \\
include 'lib/classes/site.status.php';
?>
<!DOCTYPE html>
<html>
	<head>
		<script>
			<?php echo $disable_form;?>
		</script>
		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
		<meta name="viewport" content="width=1,initial-scale=1,user-scalable=1" />
		<title>
			Inloggen - <?php echo $_CONFIG['bedrijf']; ?>
		</title>
		<link rel="shortcut icon" type="image/gif" href="<?php echo $_CONFIG['bedrijf_logo']; ?>" />
		<link rel="stylesheet" type="text/css" href="paneel/style/css/index.css" />
		<link href="http://fonts.googleapis.com/css?family=Lato:100italic,100,300italic,300,400italic,400,700italic,700,900italic,900" rel="stylesheet" type="text/css">
		<link rel="stylesheet" type="text/css" href="paneel/style/bootstrap/css/bootstrap.min.css" />
	</head>
	<body>
		<section class="container">
			<section class="login-form">
			
			<section>
				<p style="margin-bottom: -7px; margin-top: -15px; color: black; font-size: 18pt; font-weight: bold;"><?php echo $_CONFIG['bedrijf']; ?></p>
			</section>
			<div class="panel panel-default">
				<div class="panel-body">
					<form method="post" id="disable" action="" role="login">
						<div class="form-group">
							<p style="text-align: center;">Inloggen, geen account? <a class="disable" href='/registreer'>Maak er één!</a></p>
							<p style="text-align: center; color: red;"><?php echo $error; ?></p>
							<label>Gebruikersnaam</label>
							<input autofocus title="Maximaal 50 tekens." type="text" name="gebruikersnaam" maxlength="50" required class="form-control" />
						</div>
						
						<div class="form-group">
							<label>Wachtwoord</label>
							<input title="Maximaal 100 tekens." type="password" name="wachtwoord" maxlength="100" required class="form-control" />
						</div>
						
						<input type="checkbox" name="onthoud" value="1" /> Onthoud mijn gegevens.
						<button type="submit" name="inloggen" class="btn btn-primary btn-block">Inloggen</button>
					</form>
				</div>
				<div class="panel-footer">
					<a href="#">Wachtwoord vergeten?</a>
				</div>
			</div>
			<section>
				<a href="<?php $_SERVER["REQUEST_URI"]; ?>">Copyright &copy; <?php echo date("Y"); echo ' <b>'; echo $_CONFIG['bedrijf']; echo '</b>'; ?></a>
			</section>
			
			</section>
		</section>
		<script src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
		<script src="paneel/style/bootstrap/js/bootstrap.min.js"></script>
	</body>


config.php

<?php
error_reporting(0);
// DE DATABASE INFORMATIE \\
$setting = parse_ini_file('datafile.ini');
$con = new mysqli($setting['db_host'], $setting['db_user'], $setting['db_pass'], $setting['db_name']);

// CONTROLEER DE VERBINDING \\
if ($con->connect_error) {
    die('<title>Niet gelukt om te verbinden met MySQL</title>Verbinings fout (' . $con->connect_errno . ') ' . $con->connect_error); // DE ERROR ALS ER EEN FOUT IN DE VERBINDING IS \\
}

// SERVER INSTELLINGEN \\
$_SERVER['admin'] = $setting['admin'];
$_SERVER['admin_ip'] = $setting['admin_ip'];

// CONFIG INSTELLINGEN \\
$result = $con->query("SELECT * FROM `instellingen`")->fetch_array(); // SELECTEER ALLES VAN INSTELLINGEN \\
$_CONFIG['bedrijf'] = ucfirst($result['bedrijf']); // SELECTEER DE BEDRIJFSNAAM \\
$_CONFIG['bedrijf_logo'] = $result['bedrijf_logo']; // SELECTEER HET BEDRIJFS LOGO \\
?>


login_script.php

<?php
if(isset($_POST['inloggen'])){
	// DEFINITEER $GEBRUIKERSNAAM EN $WACHTWOORD \\
	$gebruikersnaam   = $_POST['gebruikersnaam'];
	$wachtwoord       = $_POST['wachtwoord'];
	
	// BESCHERM DE GEGEVENS \\
	$gebruikersnaam   = $con->real_escape_string($gebruikersnaam);
	$wachtwoord       = md5($wachtwoord);
	
	// LOGIN SCRIPT \\
	$result = $con->query("SELECT * FROM `leden` WHERE `gebruikersnaam`='$gebruikersnaam'")->fetch_array(); // SELECTEER ALLE DATA VAN DE GEBRUIKERSNAAM \\
	if (( $gebruikersnaam ) == ( $result['gebruikersnaam'] )){
		if (( $wachtwoord ) == ( $result['wachtwoord'] )){
			$id = $con->query("SELECT `id` FROM `leden` WHERE `gebruikersnaam`='$gebruikersnaam'")->fetch_array();
			$_SESSION['id'] = $id['id'];
			header('Location: /dashboard');
		}
		else{
			$error = "Er is een fout wachtwoord ingevoerd voor de gebruiker '$gebruikersnaam'.";
			header('Refresh: 2; Url=/');
		}
	}
	else{
		$error = "De gebruiker '$gebruikersnaam' kan niet worden gevonden.";
		header('Refresh: 2; Url=/');
	}
}
?>


functions.php

<?php
function logged_in(){
	if(!empty($_SESSION['id'])){
		return true;
	}
	else{
		return false;
	}
}

function logout(){
	session_unset();
	header('Location: /');
}
?>


site_status.php

<?php	
$result = $con->query("SELECT * FROM `instellingen`")->fetch_array();

$maintenanceMode      = $result['status'];
$maintananceGlobal    = $result['status2'];
$maintenanceLocations = $result['status3'];
$maintenanceMessage   = "<!DOCTYPE html>
<html>
<head>
<meta http-equiv='Content-Type' content='text/html; charset=utf-8' />
<title>Onderhoud Modus</title>
<script type='text/javascript' src='http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.js'></script>

<script type='text/javascript'>
$(document).ready(function() {	

		var id = '#dialog';
	
		//Get the screen height and width
		var maskHeight = $(document).height();
		var maskWidth = $(window).width();
	
		//Set heigth and width to mask to fill up the whole screen
		$('#mask').css({'width':maskWidth,'height':maskHeight});
		
		//transition effect		
		$('#mask').fadeIn(1000);	
		$('#mask').fadeTo('slow',0.8);	

	
		//transition effect
		$(id).fadeIn(2000); 		
		
		var fields = document.getElementById('disable').getElementsByTagName('*');
for(var i = 0; i < fields.length; i++)
{
    fields[i].disabled = true;
}
});
</script>

<style type='text/css'>
body {
font-family:verdana;
font-size:15px;
}
#overlay {
  position: fixed;
  height: 100%;
  width: 100%;
  z-index: 1000000;
  background: url('link/to/semitransparent.png');
}

a {color:#333; text-decoration:none}
a:hover {color:#ccc; text-decoration:none}

#mask {
  position:absolute;
  left:0;
  top:0;
  z-index:9000;
  background-color:#000;
  display:none;
}  
#boxes .window {
  position:absolute;
  left:0;
  top:0;
  width:340px;
  height:200px;
  display:none;
  z-index:9999;
  padding:20px;
}
#boxes #dialog {
    width: 350px;
    height: 200px;
  background-color: white;
    position: absolute !important;
    top:0 !important;
    bottom: 0 !important;
    left: 0 !important;
    right: 0 !important;
    margin: auto !important;
}
</style>
</head><body>

<div id='boxes'>
<div style='top: 199.5px; left: 551.5px; display: none;' id='dialog' class='window'>
<b style='font-size: 15pt;'><center>Onderhoud</center></b>Momenteel is het paneel in onderhoud, hierom is gebruik maken van het paneel niet mogelijk. Deze onderhoud is waarschijnlijk snel voorbij. <br><br><b>- Het Beheer</b>
</div>
<div style='width: 1478px; height: 602px; display: none; opacity: 0.8;' id='mask'></div>
</div>
</body>
</html>
"; 

if (($maintenanceMode) == "onderhoud")
{
	if (($maintananceGlobal) == "niet_overal")
	{
		if (($currentLocation) == ($maintenanceLocations))
		{
			echo $maintenanceMessage;
		}
	}
	else
	{
		echo $maintenanceMessage;
	}
}
?>



Bedankt voor jullie reacties voor het veilig te maken! PS: ik gebruik md5 omdat ik het fijn vind om te gebruiken en password_hash() werkt op een of andere manier niet.
Wordt die voor SSL gebruikt? Dan kan je beter een andere map gebruiken BOVEN je /public_html. /data bijvoorbeeld.
- Ariën - op 04/12/2016 18:34:59

Wordt die voor SSL gebruikt? Dan kan je beter een andere map gebruiken BOVEN je /public_html. /data bijvoorbeeld.


Maar alles staat in die private_html, alle bestanden. Hoe kan ik dan vanuit /private_html naar /public_html/data gaan? Ook kan ik geen link/variable zetten in e volgende code:
$setting = parse_ini_file('datafile.ini');
 dus ik kan niet 
$setting = parse_ini_file('http://DOMEIN.NL/BESTAND.php'); doen of 
$url = "http://DOMEIN.NL/BESTAND.php" $setting = parse_ini_file($url); 



[size=xsmall]Toevoeging op 04/12/2016 20:56:48:[/size]

Als je meer dan 3 keer fout hebt ingelogd. :D Na 10 minuten hoef je geen reCAPTCHA meer in te vullen.
Nu alleen nog maken dat als je succesvol bent ingelogd, dat als je uitlogd in die 10 minuten, je geen reCAPTCHA meer hoeft intevullen. Dus bruteforce kan niet meer
/private_html = Beveiligd via SSL
/public_html = Onbeveiligd via http.

Zet je site in een van beiden en de data in een bovenste directory /data.

Verder is Captcha bij elke inlog wel een beetje vervelend. Plus dat dit door bots al valt uit te lezen.
- Ariën - op 04/12/2016 22:12:20

/private_html = Beveiligd via SSL
/public_html = Onbeveiligd via http.

Zet je site in een van beiden en de data in een bovenste directory /data.

Verder is Captcha bij elke inlog wel een beetje vervelend. Plus dat dit door bots al valt uit te lezen.


Niet bij elke inlog, als je inlogd, of de capatcha goed invult krijg je hem niet meer. Tenzij je weer 3 foute inlog pogingen doet

Dus bots die re-captcha uit kunnen lezen kunnen lekker brute-forcen. Gewoon op IP-adres tijdelijk blokkeren.
- Ariën - op 04/12/2016 22:45:57

Dus bots die re-captcha uit kunnen lezen kunnen lekker brute-forcen. Gewoon op IP-adres tijdelijk blokkeren.

Hoe doe ik dat, ik kan toch ook gewoon die captcha weghalen en dat je na 5 minuten pas weer toegang hebt?
<?php
$result = $con->query("SELECT * FROM `leden` WHERE `gebruikersnaam`='$gebruikersnaam'")->fetch_array();
?>

Dit geeft foutmeldingen wanneer er iets met de tabel leden niet in de haak is. De volgorde wat je hier doet:
a) $con->query()
b) op datgene dat je van query() terugkrijgt de method fetch_array() aanroepen.

Maar wat nu als query() een FALSE terug geeft omdat je query mislukt is? FALSE->fetch_array() gaat niet werken he.

dus:
<?php
$result = $con->query("SELECT * FROM `leden` WHERE `gebruikersnaam`='$gebruikersnaam'");
if(FALSE === $result) {
// de query is mislukt.
echo 'Er gaat iets mis!';
exit;
}

// de query is geslaagd dus we gaan weer een stapje verder
$row = $result->fetch_assoc();
if(NULL === $row) {
// de query is geslaagd maar er zijn geen rijen gevonden die aan de query voldeden.
echo 'Login failed';
exit;
}
?>

Verder zou een stukje CSRF bescherming in een login formulier ook niet misstaan.

MD5 Mag je gewoon niet meer gebruiken voor user passwords.

Geef geen helpende informatie weg bij een login formulier:

De gebruiker Frank kan niet worden gevonden.

Mooi dan ga ik verder met de volgende naam. (Rainbow table)

ini en config files buiten je webroot houden

Andere think-overs:
- Gebruik TLS encryptie voor je website
- Eis een minimale wachtwoord sterkte van je gebruikers (bijv. minimale lengte 8, Minimaal 1 hoofdletter, 1 kleine letter en 1 ander leesteken)
- Twee weg identificatie
- Waarom gebruikersnamen? kan je niet met mailadressen werken?
- Moet een email en/of gebruikersnaam uniek zijn in de tabel users?
- beveilig je FTP, dashboard en Mysql server ook met een sterk wachtwoord.

<?php
// SERVER INSTELLINGEN \\
$_SERVER['admin'] = $setting['admin'];
$_SERVER['admin_ip'] = $setting['admin_ip'];
?>
Waaaaaaat? Waarom settings in een superglobal zetten ??? wat is hier het nut van?

Muggenziften:
- inline css :-(
- fetch_array() gebruikt twee keer zoveel resources dan fetch_assoc()
Rob Chesture op 04/12/2016 23:00:01

[quote="- Ariën - op 04/12/2016 22:45:57"]
Dus bots die re-captcha uit kunnen lezen kunnen lekker brute-forcen. Gewoon op IP-adres tijdelijk blokkeren.

Hoe doe ik dat, ik kan toch ook gewoon die captcha weghalen en dat je na 5 minuten pas weer toegang hebt?
[/quote]
Bij elke foutieve inlog sla je het IP op, de userID van het account en het aantal keer dat er geprobeerd is. Als dat gelijk of hoger is dan 5 bijvoorbeeld, dan blokkeer je het account op dat IP.

Maar de genoemde Two Factor authentication als extraatje is ook te te waarderen voor gebruikers. Vooral als het account voor belangrijke (zoals bedrijfsgevoelige) informatie gebruikt wordt. Net zo iets als de RaboScanner of de ABN reader.
ik mis nog de inhoud van het script waar je op komt na header('Location: /dashboard')

Check je daar ook of de gebruiker ingelogd is? (en zo nee, stuur je hem dan terug naar het inlogscript).

en op regel 12 en 15 van je inlogfunctie voer je dezelfde query uit.

Op regel 13 zit het id dus in $result['id']
beetje zinloos om dat nog een keer uit te voeren om in $id['id'] dezelfde waarde te stoppen.
Ivo P op 05/12/2016 09:55:29

ik mis nog de inhoud van het script waar je op komt na header('Location: /dashboard')

Check je daar ook of de gebruiker ingelogd is? (en zo nee, stuur je hem dan terug naar het inlogscript).

en op regel 12 en 15 van je inlogfunctie voer je dezelfde query uit.

Op regel 13 zit het id dus in $result['id']
beetje zinloos om dat nog een keer uit te voeren om in $id['id'] dezelfde waarde te stoppen.

Daar check ik of de gebruiker ingelogd is. als dat niet zo is wordt hij/zij teruggestuurd.



[size=xsmall]Toevoeging op 05/12/2016 15:24:46:[/size]


Bij elke foutieve inlog sla je het IP op, de userID van het account en het aantal keer dat er geprobeerd is. Als dat gelijk of hoger is dan 5 bijvoorbeeld, dan blokkeer je het account op dat IP.

Is niet al te handig, hiermee wordt elke keer als je page reload de query uitgevoerd. Dus als ze dan ook nog page_reload in brute_force zetten, kan de server nog steeds overbelast worden.

Reageren