Overview
Comment:Session permanente : inversion de la logique du hash : on stocke le hash en DB et on le recrée à partir du vérifieur dans le cookie, ainsi un attaquant ne peut pas voler des sessions à des utilisateurs s'il gagne accès en lecture seule à la base de données (possible si le fichier association.sqlite est accessible !)
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | dev
Files: files | file ages | folders
SHA1: b54e79d8969d6ac14fb76c36625e12c8326b477d
User & Date: bohwaz on 2017-05-11 07:09:14
Other Links: branch diff | manifest | tags
Context
2017-05-12
05:24
Recette pour lancer un serveur de développement check-in: e8409c27d6 user: bohwaz tags: dev
2017-05-11
07:09
Session permanente : inversion de la logique du hash : on stocke le hash en DB et on le recrée à partir du vérifieur dans le cookie, ainsi un attaquant ne peut pas voler des sessions à des utilisateurs s'il gagne accès en lecture seule à la base de données (possible si le fichier association.sqlite est accessible !) check-in: b54e79d896 user: bohwaz tags: dev
07:04
Stockage des dates en UTC dans Sqlite check-in: 024fc1876c user: bohwaz tags: dev
Changes

Modified src/include/lib/Garradin/Membres/Session.php from [35a4c13870] to [ba5886b165].

120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
...
162
163
164
165
166
167
168


169
170
171
172
173
174
175
176


177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
...
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348

349
350
351
352
353
354
355
356
357
358
359
360
361




362
363
364
365
366
367
368
369

370
371
372
373
374




375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
		}
		else
		{
			$user = self::createUserSession($membre->id);

			if ($permanent)
			{
				self::createPermanentSession($user);
			}

			return true;
		}
	}

	static protected function createUserSession($id)
................................................................................
		$_SESSION['user'] = $user;

		return $user;
	}

	/**
	 * Créer une session permanente "remember me"


	 * @param  \stdClass $user
	 * @return boolean
	 */
	static protected function createPermanentSession(\stdClass $user)
	{
		$selector = hash(self::HASH_ALGO, Security::random_bytes(10));
		$token = hash(self::HASH_ALGO, Security::random_bytes(10));
		$expire = (new \DateTime)->modify('+3 months');



		DB::getInstance()->insert('membres_sessions', [
			'selecteur' => $selector,
			'token'     => $token,
			'expire'    => $expire,
			'id_membre' => $user->id,
		]);

		$token = hash(self::HASH_ALGO, $token . $user->passe);
		$cookie = $selector . '|' . $token;

		$options = self::getSessionOptions();

		setcookie(self::PERMANENT_COOKIE_NAME, $cookie, $expire->getTimestamp(),
			$options['cookie_path'], $options['cookie_domain'], $options['cookie_secure'], true);

		return true;
................................................................................
		if (count($data) !== 2)
		{
			return false;
		}

		return (object) [
			'selector' => $data[0],
			'token'    => $data[1],
		];
	}

	/**
	 * Connexion automatique en utilisant un cookie permanent
	 * (fonction "remember me")
	 *
	 * @link   https://www.databasesandlife.com/persistent-login/
	 * @link   https://paragonie.com/blog/2015/04/secure-authentication-php-with-long-term-persistence

	 * @link   http://jaspan.com/improved_persistent_login_cookie_best_practice
	 * @return boolean
	 */
	protected function autoLogin()
	{
		$cookie = $this->getPermanentCookie();

		if (!$cookie)
		{
			return false;
		}

		$db = DB::getInstance();




		$row = $db->first('SELECT ms.token, ms.id_membre, 
			strftime("%s", ms.expire) AS expire, membres.passe
			FROM membres_sessions AS ms
			INNER JOIN membres ON membres.id = ms.id_membre
			WHERE ms.selecteur = ?;',
			$cookie->selector);

		if ($row->expire < time())

		{
			var_dump($row, time());
			die('expired');
			return $this->logout();
		}





		// On utilise le mot de passe: si l'utilisateur change de mot de passe
		// toutes les sessions précédentes sont invalidées
		$hash = hash(self::HASH_ALGO, $row->token . $row->passe);

		// Vérification du token
		if (!hash_equals($cookie->token, $hash))
		{
			// Le sélecteur est valide, mais pas le token ?
			// c'est probablement que le cookie a été volé, qu'un attaquant
			// a obtenu un nouveau token, et que l'utilisateur se représente 
			// avec un token qui n'est plus valide.
			// Dans ce cas supprimons toutes les sessions de ce membre pour 
			// le forcer à se re-connecter
			$db->delete('membres_sessions', 'id_membre = :id', ['id' => (int) $row->id_membre]);

			return $this->logout();
		}

		// Re-générons un nouveau token et mettons à jour le cookie
		$token = hash(self::HASH_ALGO, Security::random_bytes(10));
		$expire = (new \DateTime)->modify('+3 months');

		$db->update('membres_sessions', [
			'token'     => $token,
			'expire'    => $expire,
		], 'selecteur = :selecteur AND id_membre = :id_membre', [
			'selecteur' => $cookie->selector,
			'id_membre' => $row->id_membre,
		]);

		$hash = hash(self::HASH_ALGO, $token . $row->passe);
		$new_cookie = $cookie->selector . '|' . $hash;

		$options = self::getSessionOptions();

		setcookie(self::PERMANENT_COOKIE_NAME, $new_cookie, $expire->getTimestamp(),
			$options['cookie_path'], $options['cookie_domain'], $options['cookie_secure'], true);


		$this->id = $row->id_membre;

		self::createUserSession($this->id);

		return true;
	}

	public function logout()







|







 







>
>






|

>
>



|




<
|







 







|









>













>
>
>
>
|






|
>

<
<


>
>
>
>



|


|







<




|
|
|

<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|







120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
...
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188

189
190
191
192
193
194
195
196
...
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379


380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399

400
401
402
403
404
405
406
407

















408
409
410
411
412
413
414
415
		}
		else
		{
			$user = self::createUserSession($membre->id);

			if ($permanent)
			{
				self::createPermanentSession($membre);
			}

			return true;
		}
	}

	static protected function createUserSession($id)
................................................................................
		$_SESSION['user'] = $user;

		return $user;
	}

	/**
	 * Créer une session permanente "remember me"
	 *
	 * @see autoLogin method
	 * @param  \stdClass $user
	 * @return boolean
	 */
	static protected function createPermanentSession(\stdClass $user)
	{
		$selector = hash(self::HASH_ALGO, Security::random_bytes(10));
		$verifier = hash(self::HASH_ALGO, Security::random_bytes(10));
		$expire = (new \DateTime)->modify('+3 months');

		$hash = hash(self::HASH_ALGO, $selector . $verifier . $user->passe . $expire->format(DATE_ATOM));

		DB::getInstance()->insert('membres_sessions', [
			'selecteur' => $selector,
			'hash'      => $hash,
			'expire'    => $expire,
			'id_membre' => $user->id,
		]);


		$cookie = $selector . '|' . $verifier;

		$options = self::getSessionOptions();

		setcookie(self::PERMANENT_COOKIE_NAME, $cookie, $expire->getTimestamp(),
			$options['cookie_path'], $options['cookie_domain'], $options['cookie_secure'], true);

		return true;
................................................................................
		if (count($data) !== 2)
		{
			return false;
		}

		return (object) [
			'selector' => $data[0],
			'verifier' => $data[1],
		];
	}

	/**
	 * Connexion automatique en utilisant un cookie permanent
	 * (fonction "remember me")
	 *
	 * @link   https://www.databasesandlife.com/persistent-login/
	 * @link   https://paragonie.com/blog/2015/04/secure-authentication-php-with-long-term-persistence
	 * @link   https://paragonie.com/blog/2017/02/split-tokens-token-based-authentication-protocols-without-side-channels
	 * @link   http://jaspan.com/improved_persistent_login_cookie_best_practice
	 * @return boolean
	 */
	protected function autoLogin()
	{
		$cookie = $this->getPermanentCookie();

		if (!$cookie)
		{
			return false;
		}

		$db = DB::getInstance();

		// Suppression des sessions qui ont expiré déjà
		$db->delete('membres_sessions', 'expire < strftime(\'%s\',\'now\')');
		
		$row = $db->first('SELECT ms.hash, ms.id_membre AS id, 
			strftime("%s", ms.expire) AS expire, membres.passe
			FROM membres_sessions AS ms
			INNER JOIN membres ON membres.id = ms.id_membre
			WHERE ms.selecteur = ?;',
			$cookie->selector);

		// Le sélecteur n'est pas valide: supprimons le cookie
		if (!$row)
		{


			return $this->logout();
		}

		// La session stockée ne sert plus à rien à partir de maintenant,
		// et ça empêche de le rejouer
		$db->delete('membres_sessions', 'selecteur = ?', $cookie->selector);

		// On utilise le mot de passe: si l'utilisateur change de mot de passe
		// toutes les sessions précédentes sont invalidées
		$hash = hash(self::HASH_ALGO, $cookie->selector . $cookie->verifier . $row->passe . date(DATE_ATOM, $row->expire));

		// Vérification du token
		if (!hash_equals($row->hash, $hash))
		{
			// Le sélecteur est valide, mais pas le token ?
			// c'est probablement que le cookie a été volé, qu'un attaquant
			// a obtenu un nouveau token, et que l'utilisateur se représente 
			// avec un token qui n'est plus valide.
			// Dans ce cas supprimons toutes les sessions de ce membre pour 
			// le forcer à se re-connecter


			return $this->logout();
		}

		// Re-générons un nouveau vérifieur et mettons à jour le cookie
		// car chaque vérifieur est à usage unique
		self::createPermanentSession($row);


















		$this->id = $row->id;

		self::createUserSession($this->id);

		return true;
	}

	public function logout()