Kirjautuminen

Haku

Tehtävät

Keskustelu: Nettisivujen teko: Kirjautumisen tietoturva

Sivun loppuun

dartvaneri [28.10.2014 14:05:56]

#

Moro!
Tiedän, näitä keskusteluja on pilvinpimein, mutta haluan varmistaa, että oon ymmärtänyt asiat oikein, ja kysellä vähän toteutuksesta.

Tässä alussa yritän vähän selittää ideaa, ja lopussa on sitten koodit.

Tietokannassa mulla on seuraavat taulut, sarakkeineen:
-user -taulu sisältää käyttäjän tiedot.
*id
*name
*hash
*salt
-login -taulu sisältää kirjautumisen tiedot.
*userID
*sessionID
*lastLogin

Salasanojen tiivistämiseen käytetään SHA1-funktiota. Tiivisteeseen lasketaan käyttäjän salasana, uniqid-funktion paluuarvo ja käyttäjän id. Tietokantaan tallennetaan tiiviste ja suola. Joka kirjautumiskerralla luodaan käyttäjälle uusi suola ja lasketaan uusi tiiviste.

Kirjautuessa luodaan käyttäjälle uniikki instuntotunnus, joka tallennetaan sessionID -istuntoon ja tietokantaan. Joka sivunlatauksella tarkistetaan täsmääkö kannassa oleva ja istunnossa oleva istuntotunnus, jos ei täsmää, kirjataan käyttäjä ulos.

SQL-injektiot estetään laittamalla kyselyjen parametrit execute-metodien kautta kyselyyn.

Selittely ei ole mikään vahvuuteni, joten koodi kerro tärkeimmän.
Avaan tietokanta yhteyden luokassa DB:

<?php
	class DB{
		private $db;

		public function __construct(){
			try{
				//Avataan tietokanta yhteys
				$this->db = new PDO("mysql:host=localhost;dbname=x", "x", "x");
			}
			catch(PDOException $error) {
				die("VIRHE: " . $error->getMessage());	//Tuolstetaan virheilmoitus.
			}
			$this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
			$this->db->exec("SET NAMES utf8"); //Asetetaan merkistö

			return $this->db; //Palautetaan tietokanta-olio.
		}
	}
?>

Sitten minulla on luokka Login, jossa on kaikki kirjautumisen metodit.

class Login extends DB{
	private $password, $username, $userID;
	private $db;

	public function __construct($password, $username){
		$this->password = $password;
		$this->username = $username;

		$this->db = parent::__construct(); //Avataan tietokantayhteys.
	}
}

Joka sisältää metodin login, jota käytetään kirjautumiseen.

public function login(){
	//Haetaan käyttäjän tiedot.
	$getUser = $this->db->prepare("SELECT * FROM user WHERE name = ?");
	$getUser->execute(array($this->username));

	if($getUser->rowCount() == 0){ //Jos yhtään tunnusta ei löydy.
		return array(false, "Tunnusta ei löytynyt!");
	} else {
		$userData = $getUser->fetchObject();

		//Jos tiivisteet täsmäävät.
		if($userData->hash == SHA1($this->password.$userData->salt.$userData->id)){
			$newSalt = uniqid("", true); //Luodaan uusi suola.
			$newHash = SHA1($this->password.$newSalt.$userData->id); //Luodaan tiiviste salasanasta ja uudesta suolasta.

			$sessionID = uniqid("", true); //Luodaan uusi istuntotunnus

			//Päivitetään uusi suola ja tiiviste kantaan.
			$updateUserData = $this->db->prepare("UPDATE user SET salt = ?, hash = ? WHERE id = ?");
			$updateUserData->execute(array($newSalt, $newHash, $userData->id));

			//Päivitetään uusi istuntotunnus kantaan.
			$updateUserData2 = $this->db->prepare("UPDATE login SET sessionID = ? WHERE userID = ?");
			$updateUserData2->execute(array($sessionID, $userData->id));

			if($updateUserData && $updateUserData2){ //Jos molemmat kyselyt onnistuivat.
				$_SESSION["sessionID"] = $sessionID; //Luodaan sessionID -istunto.
				$_SESSION["user"] = $userData->id; //Luodaan user -istunto.

				return array(true);
			} else {
				return array(false, "Kyselyssä tapahtui virhe!");
			}
		} else {
			return array(false, "Salasana väärin!");
		}
	}
}

Lisäksi metodin isLoggedIn, joka kertoo, onko käyttäjä kirjautunut, se myös tarkistaa, onko sessionID oikein.

public function isLoggedIn(){
	if(isset($_SESSION["user"])){ //Jos user istunto on olemassa.
		//Haetaan kannasta nykyinen istuntotunnus.
		$getSessionID = $this->db->prepare("SELECT sessionID FROM login WHERE userID = ?");
		$getSessionID->execute(array($_SESSION['user']));

		$result = $getSessionID->fetchObject();
		//Jos kannasta haettu istuntotunnus täsmää olemassa oleva istuntotunnuksen kanssa.
		if($result->sessionID == $_SESSION["sessionID"]){
			//Päivitetään viimeisin lataus aika kantaan.
			$updateLastLoad = $this->db->prepare("UPDATE login SET lastLoad = NOW() WHERE userID = ?");
			$updateLastLoad->execute(array($_SESSION['user']));

			return true;
		}
	}
	return false;
}

Luokka sisältää myös metodin logout, jota käytetään käyttäjän uloskirjaamiseen.

public function logout(){
	//Tuhotaan istunnot.
	unset($_SESSION['sessionID']);
	unset($_SESSION['user']);
}

Luokan käyttö:

<?php
	session_start();
	include("Login.php");
	$username = !isset($_REQUEST["username"]) ? "" : $_REQUEST["username"];
	$password = !isset($_REQUEST["password"]) ? "" : $_REQUEST["password"];

	$login = new Login($password, $username);

	//Kirjautuminen
	if(isset($_REQUEST['login'])){
		$return = $login->login();
		if(!$return[0]){
			$return[1];
		}
	}

	//Ulos kirjautuminen
	if(isset($_REQUEST['logout'])){
		$login->logout();
	}

	//Tarkistetaan onko käyttäjä kirjautunut
	if($login->isLoggedIn()){
		echo "Tervetuloa! <a href='?logout'>Kirjaudu ulos!</a>";
	} else {
		$login->logout();
?>
<form action="?login" method="post">
	<input type="text" name="username"><br>
	<input type="password" name="password"><br>
	<input type="submit" value="Kirjaudu">
</form>
<?php
	}
?>

Olen toteuttanut tämän oophp:llä, jota en juurikaan ole harrastanut, siksi kysynkin, että onko tuo järkevästi toteutettu? Onko jotain, mikä olisi järkevämpi tehdä jollain muulla tavalla?

Pelkkä kirjautumisen tietoturvahan ei luonnollisesti riitä sivuston tietoturvaksi, mutta onhan se jo hyvä alku.

Eki++ [28.10.2014 14:31:26]

#

PHP5:ssä on mukana uudet salasanafunktiot. Ne huolehtivat itse suolaamisesta ja sopivista algoritmeista. Katso vinkki. Muita puutteita/virheitä en ekalla silmäyksellä havainnut.

RQ [28.10.2014 14:43:13]

#

Ei näyttäisi olevan minkään näköistä suojausta CSRF vastaan.

dartvaneri [28.10.2014 15:07:06]

#

Eki++, olen kyllä tietoinen funktioista, mutta kun sattuu olemaan palvelimella PHP 5.3.29, niin eipäs taida toimia.

Kuinkas tuo CSRF-hyökkäys on tässä tapauksessa mahdollinen?

RQ [28.10.2014 15:21:59]

#

dartvaneri kirjoitti:

Kuinkas tuo CSRF-hyökkäys on tässä tapauksessa mahdollinen?

https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet­#Only_Accepting_POST_Requests:

The misconception is that since the attacker cannot construct a malicious link, a CSRF attack cannot be executed. Unfortunately, this logic is incorrect. There are numerous methods in which an attacker can trick a victim into submitting a forged POST request, such as a simple form hosted in an attacker's Website with hidden values. This form can be triggered automatically by JavaScript or can be triggered by the victim who thinks the form will do something else.

Eli POST-metodi ei suojaa CSRF:ltä vaan se voidaan toteuttaa myös formin avulla.

Edit: Toisaalta eipä tuossa kauheasti CSRF:lle ole käyttöä, kun ei minkään näköistä toiminnallisuuttakaan löydy. Toki käyttäjän voi uloskirjata http://esimerkki/?logoutin kautta.

dartvaneri [28.10.2014 15:45:14]

#

RQ kirjoitti:

Toki käyttäjän voi uloskirjata http://esimerkki/?logoutin kautta.

Onko siitä haittaa? Mikäli käyttäjä näppäilee noin osoiteriville, uskosin sen tekevän sen tarkoituksella.

Metabolix [28.10.2014 16:01:55]

#

dartvaneri kirjoitti:

Eki++, olen kyllä tietoinen [salasana]funktioista, mutta kun sattuu olemaan palvelimella PHP 5.3.29, niin eipäs taida toimia.

Kuten koodivinkissä lukee, lataa yhteensopivuuskirjasto. Ei ole järkeä kikkailla omia. Lisäksi PHP 5.3:n käytöstä voisi hyvin tehdä valituksen webhotellille, koska kyseiselle versiolle ei aktiivisesti tehdä enää edes tietoturvapäivityksiä.

Salasanan tiivistämiseen ei kannata käyttää nopeita tiivistefunktioita kuten SHA1-funktiota (ilman iterointia). Käyttäjän ID:n lisääminen syötteeseen ei olennaisesti paranna tietoturvaa. Uniqid ei ole kunnolla satunnainen vaan sisältää lähinnä kellonajan ja erikseen pyydettäessä yhden heikon satunnaisluvun, joten siltä ei saa hyvää suolaa eikä hyvää istunnon tunnusta; parempaa satunnaisdataa antaa vaikka /dev/urandom tai openssl_random_pseudo_bytes, ja istunnon tunnukseen (tai muuten vain evästeisiin) voi liittää liittää myös käyttäjän ID:n äärimmäisen epätodennäköisen törmäyksen välttämiseksi.

dartvaneri [28.10.2014 16:18:08]

#

Koodivinkki kirjoitti:

Uudet funktiot lisättiin PHP:hen versiossa 5.5. Vanhempiin versioihin 5.3.7:stä alkaen ne voi ladata GitHubista ja liittää mukaan include-komennolla.

dartvaneri kirjoitti:

Eki++, olen kyllä tietoinen funktioista, mutta kun sattuu olemaan palvelimella PHP 5.3.29, niin eipäs taida toimia.

Tarkoittaako tämä nyt sitä, että koodivinkissä on virhe, vai että sulla jäi jotain huomaamatta?

Metabolix kirjoitti:

Salasanan tiivistämiseen ei kannata käyttää nopeita tiivistefunktioita kuten SHA1-funktiota (ilman iterointia).

Olisko parempaa ideaa, jos kerran nuita PHP:n uusia funktioita ei saa käyttöön?
Tarkoittaako iterointi nyt tässä tapauksessa sitä, että laitetaan SHA1-funktioita x-määrä sisäkkäin(vaikuttaa typerältä)? Vai?

Metabolix [28.10.2014 17:23:38]

#

dartvaneri kirjoitti:

5.3.7 – – 5.3.29 – – Tarkoittaako tämä nyt sitä, että koodivinkissä on virhe, vai että sulla jäi jotain huomaamatta?

Vai etkö ehkä itse huomannut, että 5.3.29 on peräti kolme vuotta ja 22 versionumeroa uudempi kuin 5.3.7?

dartvaneri kirjoitti:

Tarkoittaako iterointi nyt tässä tapauksessa sitä, että laitetaan SHA1-funktioita x-määrä sisäkkäin(vaikuttaa typerältä)? Vai?

Kyllä, paitsi yleensä käytetään silmukkaa, koska ei todellakaan puhuta muutamasta toistosta vaan esimerkiksi 10000 toistosta. Oikeat algoritmit salasanojen tiivistämiseen – esimerkiksi PHP:n käyttämä bcrypt – sisältävät valmiiksi jonkinlaista toistoa, kun taas SHA1 on suunniteltu nopeaksi, jotta sillä voi käsitellä suurenkin datamäärän. Hitaus on valttia, koska silloin hyökkääjä ei pysty murtamaan salasanaa kokeilemalla.

RQ [28.10.2014 17:48:05]

#

dartvaneri kirjoitti:

RQ kirjoitti:

Toki käyttäjän voi uloskirjata http://esimerkki/?logoutin kautta.

Onko siitä haittaa? Mikäli käyttäjä näppäilee noin osoiteriville, uskosin sen tekevän sen tarkoituksella.

Tarkoitin sitä CSRF:n yhteydessä, ulkopuolinen käyttäjä voi kirjata toisen käyttäjän ulos tuolla linkillä.

Metabolix [28.10.2014 18:09:55]

#

dartvaneri kirjoitti:

$_SESSION['sessionID']

Huomasin tämän kohdan vasta nyt. Koko sessionID-viritelmäsi on täydellisen turha. PHP:n istuntojen data sijaitsee palvelimella, käyttäjä ei voi muokata sitä, ja mitään ei silloin tarvitse tarkistaa. PHP generoi istunnolle automaattisesti tunnisteen, tallentaa sen evästeeseen ja avaa sen perusteella oikean istunnon kutsulla session_start. Jos pyydettyä istuntoa ei löydy, $_SESSION on tyhjä. Istunnot tallennetaan tavallisesti levylle tai muistiin, mutta omilla kikoilla ne voi laittaa myös tietokantaan.

p99o [28.10.2014 19:40:48]

#

Itselläni on omat viritelmät jossa metodi, joka tarkistaa kirjautumisen, palauttaa käyttäjän ID:n (siis 0 / false jos ei aktiivista istuntoa)

isLoggedIn()

// tarkasta istunto ( == 0 tai false)
if($session->isLoggedIn() != 0) {

}

voisi korvata esim getSessionUserid()

lisäksi olisi vain yksi eväste esim.

$_COOKIE['sessionkey']

mikäli homma on tietokantapohjainen, ei session-evästeillä hoidettu.

Deffi [28.10.2014 22:34:02]

#

dartvaneri kirjoitti:

RQ kirjoitti:

Toki käyttäjän voi uloskirjata http://esimerkki/?logoutin kautta.

Onko siitä haittaa? Mikäli käyttäjä näppäilee noin osoiteriville, uskosin sen tekevän sen tarkoituksella.

Ehkä ymmärrät paremmin mistä CSRF:ssä on kyse jos katsot tämän kuvan: kissa.jpg

Grez [28.10.2014 22:38:13]

#

Muuten hyvä mutta eihän missä se CS tuossa esimerkissä on?

Deffi [28.10.2014 22:43:58]

#

Grez kirjoitti:

Muuten hyvä mutta missä se CS tuossa esimerkissä on?

Joo, CS tarvitaan jos pitää täyttää ja lähettää POST-lomake.

edit. lisäsin nyt kuitenkin ulkoisen linkin.

The Alchemist [29.10.2014 10:12:35]

#

p99o kirjoitti:

Itselläni on omat viritelmät jossa metodi, joka tarkistaa kirjautumisen, palauttaa käyttäjän ID:n (siis 0 / false jos ei aktiivista istuntoa)

isLoggedIn()

// tarkasta istunto ( == 0 tai false)
if($session->isLoggedIn() != 0) {

}

voisi korvata esim getSessionUserid()

lisäksi olisi vain yksi eväste esim.

$_COOKIE['sessionkey']

mikäli homma on tietokantapohjainen, ei session-evästeillä hoidettu.

Sessiot eivät ole evästeitä. Samalla tavalla php:n natiivia sessiota käyttäessä tarvitaan (vain) yksi session avaimen ilmoittava eväste eli cookie. Sessio laajentaa evästeitä tarjoamalla mahdollisuuden käyttää istuntokohtaisia tietoja, jotka säilytetään palvelimella ja joita käyttäjälle ei koskaan paljasteta/lähetetä suoraan.

p99o [29.10.2014 12:12:34]

#

Miksi sitten puhutaan "session cookie":ista. ja kyllä tiedän miten $_session-toimii

RQ [29.10.2014 13:06:32]

#

p99o kirjoitti:

Miksi sitten puhutaan "session cookie":ista. ja kyllä tiedän miten $_session-toimii

Siksi, että sessionin tunnus tallennetaan evästeisiin. Itse sessionin sisältö löytyy palvelimelta.

timoh [29.10.2014 13:09:39]

#

p99o kirjoitti:

Miksi sitten puhutaan "session cookie":ista

Se sisältää uniikin ja ennalta arvaamattoman ID:n minkä perusteella sessiossa oleva data yhdistetään juuri kyseisen cookien haltijaan.

p99o [29.10.2014 13:57:06]

#

Tiedän kyllä, kuten jo sanoin.

RQ [29.10.2014 14:06:52]

#

Miksi sitten kysyä jos tietää...?

dartvaneri [29.10.2014 14:39:48]

#

Metabolix kirjoitti:

dartvaneri kirjoitti:

5.3.7 – – 5.3.29 – – Tarkoittaako tämä nyt sitä, että koodivinkissä on virhe, vai että sulla jäi jotain huomaamatta?

Vai etkö ehkä itse huomannut, että 5.3.29 on peräti kolme vuotta ja 22 versionumeroa uudempi kuin 5.3.7?

Niinpä tietenki.. -.-
Ajattelin jotenki 'hienosti, että 5.3.2(9) on vanhempi kuin 5.3.7.

Metabolix kirjoitti:

dartvaneri kirjoitti:

$_SESSION['sessionID']

Huomasin tämän kohdan vasta nyt. Koko sessionID-viritelmäsi on täydellisen turha. PHP:n istuntojen data sijaitsee palvelimella, käyttäjä ei voi muokata sitä, ja mitään ei silloin tarvitse tarkistaa. PHP generoi istunnolle automaattisesti tunnisteen, tallentaa sen evästeeseen ja avaa sen perusteella oikean istunnon kutsulla session_start. Jos pyydettyä istuntoa ei löydy, $_SESSION on tyhjä. Istunnot tallennetaan tavallisesti levylle tai muistiin, mutta omilla kikoilla ne voi laittaa myös tietokantaan.

Tarkoitatko nyt tällä sitä, että riittää pelkästään tuo user-istunto? Entä kuinkas, sitten jos halutaan estää kirjautuminen kahdelta koneelta yhtäaikaa?

timoh [29.10.2014 15:20:29]

#

Pomminvarmasti et voi estää mitenkään etteikö samaa istuntoa voisi käyttää useammalta koneelta.

Ja esim. kuinka käy käyttäjän joka oli kirjautuneena mutta jonka kone tilttasi (sitten kun hän koittaa ilman vanhaa istuntotunnistetta tulla takaisin uudelleen)?

Mutta pääpiirteittäin tallennat tiedot kirjautuneista käyttäjistä, ja jos sama käyttäjätunnus koittaa kirjautua sisään lomakkeelta uudelleen, niin estät kirjautumisen.

The Alchemist [29.10.2014 19:31:58]

#

Miksi kukaan haluaisi estää ihmisiä käyttämästä palvelua monelta koneelta? Minusta tuo on typerä ajattelumalli. Päin vastoin pitäisi aina varmistaa, ettei yhdeltä koneelta kirjautuminen heitä pihalle toisaalla. Tuolla ei ole mitään tekemistä tietoturvan kanssa vaan se kuuluu käytettävyyden piiriin. On huonoa käytettävyyttä, jos kännykällä kirjautumisen takia pöytäkoneen sessio katkaistaan.

p99o [29.10.2014 21:34:36]

#

RQ kirjoitti:

Miksi sitten kysyä jos tietää...?

koska en ymmärtänyt mitä tarkoittaa "sessiot eivät ole evästeitä". Ehkäpä ne sitten eivät ole evästeitä vaan hieman jotain muuta, vaikka sellaiseksi ne mielestäni mielletään, toiminnasta itsessään en halua luentoa.
No olivat mitä olivat, en jaksa kiinnostua.

Metabolix [29.10.2014 21:43:47]

#

dartvaneri kirjoitti:

Tarkoitatko nyt tällä sitä, että riittää pelkästään tuo user-istunto?

Yritä nyt ottaa termit jotenkin haltuun. Ei sinulla ole mitään ”user-istuntoa”. Sinulla on istunto, ja istunnon myötä $_SESSION-taulukko sisältää arvoja.

dartvaneri kirjoitti:

Entä kuinkas, sitten jos halutaan estää kirjautuminen kahdelta koneelta yhtäaikaa?

Miksi haluttaisiin? Vaikuttaa aivan turhalta rajoitukselta.

Jos tunnet jotain pakottavaa tarvetta tähän, voit kirjautumisen yhteydessä tallentaa tietokantaan arvon session_id() ja muissa kohdissa tarkistaa tuon samaisen arvon:

if (foo_sql("SELECT 1 FROM user WHERE id = ? AND session_id = ?", $_SESSION["user_id"], session_id()) != 1) {
	// Väärä istunto, kirjataan ulos.
}

p99o kirjoitti:

Tiedän kyllä miten isuntoeväste eroaa toiminnaltaa normievästeestä,

Kiinnostavaa, sillä itse tiedän, että se ei eroa toiminnaltaan mitenkään. (Ahaa, muokkasit viestiäsi.) Tietääkseni ei myöskään mielletä istuntoja (jos tarkoitat PHP:n $_SESSION-muuttujan sisältöä) evästeiksi; se on yksiselitteisesti väärin ajateltu.

p99o [29.10.2014 21:50:20]

#

Tietääkseni session tuhoutuu isutunnon päätteeksi, eikä sessionin muokkaaminen hyödytä mitään, koska se tarkastetaan kuitenkin palvelimen päässä. Tai sitten sekoitan istuntokohtaiset evästeet (ne ovat 'session cookie') ja istunnon keskenään.

Metabolix [29.10.2014 22:04:36]

#

p99o: Istuntokohtaiset evästeet eivät liity mitenkään PHP:n $_SESSION-muuttujaan tai session-alkuisiin funktioihin. $_SESSION on palvelimella, käyttäjä ei voi muokata sitä. Evästeet taas ovat selaimessa, niitä voi muokata, ja niitä ei mitenkään tarkisteta palvelimella – jos palvelin tietäisi oikeat arvot, mitä järkeä olisi ylipäänsä siirtää niitä selaimelle ja takaisin?

p99o [29.10.2014 23:02:12]

#

Metabolix kirjoitti:

p99o: Istuntokohtaiset evästeet eivät liity mitenkään PHP:n $_SESSION-muuttujaan

no näinhän se sitten on

The Alchemist [30.10.2014 09:32:34]

#

p99o kirjoitti:

Tietääkseni session tuhoutuu isutunnon päätteeksi, eikä sessionin muokkaaminen hyödytä mitään, koska se tarkastetaan kuitenkin palvelimen päässä. Tai sitten sekoitan istuntokohtaiset evästeet (ne ovat 'session cookie') ja istunnon keskenään.

"Session cookie" ei liene ilmaisuna mitenkään standardi, mutta itse ymmärtäisin sillä tarkoitettavan sitä yhtä ainoaa evästettä, joka kertoo php:lle, minkä istunnon data pitää lukea $_SESSION-muuttujaan suorituksen alussa. Kyseisen evästeen nimi on oletuksena PHPSESSID ja se löytyy kaikista selaimista, joilla on käyty jollain sivulla, jonka koodissa kutsut session_start()-funktiota. Istuntodatan säilyttäminen on php:n oma ominaisuus eikä liity selaimiin eikä käytettyyn palvelinsovellukseen (esim. Apache).

Sessiot eivät itse asiassa tuhoudu (välittömästi) istunnon päätteeksi, mikäli et kutsu session_destroy()-funkkaria. Vanhat istunnot jäävät levylle talteen, kunnes php:n roskankerääjä pyörähtää ja poistaa vanhentuneiksi tiedetyt istunnot.

Metabolix [30.10.2014 18:42:38]

#

The Alchemist kirjoitti:

"Session cookie" ei liene ilmaisuna mitenkään standardi, mutta itse ymmärtäisin sillä tarkoitettavan sitä yhtä ainoaa evästettä, joka kertoo php:lle – –

Usein kylläkin session cookie tarkoittaa evästettä, jolla ei ole määrättyä päättymisajankohtaa vaan jonka selain yleensä poistaa sulkemisen yhteydessä.


Sivun alkuun

Vastaus

Aihe on jo aika vanha, joten et voi enää vastata siihen.

Tietoa sivustosta