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    120   		}
   121    121   		else
   122    122   		{
   123    123   			$user = self::createUserSession($membre->id);
   124    124   
   125    125   			if ($permanent)
   126    126   			{
   127         -				self::createPermanentSession($user);
          127  +				self::createPermanentSession($membre);
   128    128   			}
   129    129   
   130    130   			return true;
   131    131   		}
   132    132   	}
   133    133   
   134    134   	static protected function createUserSession($id)
................................................................................
   162    162   		$_SESSION['user'] = $user;
   163    163   
   164    164   		return $user;
   165    165   	}
   166    166   
   167    167   	/**
   168    168   	 * Créer une session permanente "remember me"
          169  +	 *
          170  +	 * @see autoLogin method
   169    171   	 * @param  \stdClass $user
   170    172   	 * @return boolean
   171    173   	 */
   172    174   	static protected function createPermanentSession(\stdClass $user)
   173    175   	{
   174    176   		$selector = hash(self::HASH_ALGO, Security::random_bytes(10));
   175         -		$token = hash(self::HASH_ALGO, Security::random_bytes(10));
          177  +		$verifier = hash(self::HASH_ALGO, Security::random_bytes(10));
   176    178   		$expire = (new \DateTime)->modify('+3 months');
          179  +
          180  +		$hash = hash(self::HASH_ALGO, $selector . $verifier . $user->passe . $expire->format(DATE_ATOM));
   177    181   
   178    182   		DB::getInstance()->insert('membres_sessions', [
   179    183   			'selecteur' => $selector,
   180         -			'token'     => $token,
          184  +			'hash'      => $hash,
   181    185   			'expire'    => $expire,
   182    186   			'id_membre' => $user->id,
   183    187   		]);
   184    188   
   185         -		$token = hash(self::HASH_ALGO, $token . $user->passe);
   186         -		$cookie = $selector . '|' . $token;
          189  +		$cookie = $selector . '|' . $verifier;
   187    190   
   188    191   		$options = self::getSessionOptions();
   189    192   
   190    193   		setcookie(self::PERMANENT_COOKIE_NAME, $cookie, $expire->getTimestamp(),
   191    194   			$options['cookie_path'], $options['cookie_domain'], $options['cookie_secure'], true);
   192    195   
   193    196   		return true;
................................................................................
   332    335   		if (count($data) !== 2)
   333    336   		{
   334    337   			return false;
   335    338   		}
   336    339   
   337    340   		return (object) [
   338    341   			'selector' => $data[0],
   339         -			'token'    => $data[1],
          342  +			'verifier' => $data[1],
   340    343   		];
   341    344   	}
   342    345   
   343    346   	/**
   344    347   	 * Connexion automatique en utilisant un cookie permanent
   345    348   	 * (fonction "remember me")
   346    349   	 *
   347    350   	 * @link   https://www.databasesandlife.com/persistent-login/
   348    351   	 * @link   https://paragonie.com/blog/2015/04/secure-authentication-php-with-long-term-persistence
          352  +	 * @link   https://paragonie.com/blog/2017/02/split-tokens-token-based-authentication-protocols-without-side-channels
   349    353   	 * @link   http://jaspan.com/improved_persistent_login_cookie_best_practice
   350    354   	 * @return boolean
   351    355   	 */
   352    356   	protected function autoLogin()
   353    357   	{
   354    358   		$cookie = $this->getPermanentCookie();
   355    359   
   356    360   		if (!$cookie)
   357    361   		{
   358    362   			return false;
   359    363   		}
   360    364   
   361    365   		$db = DB::getInstance();
   362         -		$row = $db->first('SELECT ms.token, ms.id_membre, 
          366  +
          367  +		// Suppression des sessions qui ont expiré déjà
          368  +		$db->delete('membres_sessions', 'expire < strftime(\'%s\',\'now\')');
          369  +		
          370  +		$row = $db->first('SELECT ms.hash, ms.id_membre AS id, 
   363    371   			strftime("%s", ms.expire) AS expire, membres.passe
   364    372   			FROM membres_sessions AS ms
   365    373   			INNER JOIN membres ON membres.id = ms.id_membre
   366    374   			WHERE ms.selecteur = ?;',
   367    375   			$cookie->selector);
   368    376   
   369         -		if ($row->expire < time())
          377  +		// Le sélecteur n'est pas valide: supprimons le cookie
          378  +		if (!$row)
   370    379   		{
   371         -			var_dump($row, time());
   372         -			die('expired');
   373    380   			return $this->logout();
   374    381   		}
          382  +
          383  +		// La session stockée ne sert plus à rien à partir de maintenant,
          384  +		// et ça empêche de le rejouer
          385  +		$db->delete('membres_sessions', 'selecteur = ?', $cookie->selector);
   375    386   
   376    387   		// On utilise le mot de passe: si l'utilisateur change de mot de passe
   377    388   		// toutes les sessions précédentes sont invalidées
   378         -		$hash = hash(self::HASH_ALGO, $row->token . $row->passe);
          389  +		$hash = hash(self::HASH_ALGO, $cookie->selector . $cookie->verifier . $row->passe . date(DATE_ATOM, $row->expire));
   379    390   
   380    391   		// Vérification du token
   381         -		if (!hash_equals($cookie->token, $hash))
          392  +		if (!hash_equals($row->hash, $hash))
   382    393   		{
   383    394   			// Le sélecteur est valide, mais pas le token ?
   384    395   			// c'est probablement que le cookie a été volé, qu'un attaquant
   385    396   			// a obtenu un nouveau token, et que l'utilisateur se représente 
   386    397   			// avec un token qui n'est plus valide.
   387    398   			// Dans ce cas supprimons toutes les sessions de ce membre pour 
   388    399   			// le forcer à se re-connecter
   389         -			$db->delete('membres_sessions', 'id_membre = :id', ['id' => (int) $row->id_membre]);
   390    400   
   391    401   			return $this->logout();
   392    402   		}
   393    403   
   394         -		// Re-générons un nouveau token et mettons à jour le cookie
   395         -		$token = hash(self::HASH_ALGO, Security::random_bytes(10));
   396         -		$expire = (new \DateTime)->modify('+3 months');
          404  +		// Re-générons un nouveau vérifieur et mettons à jour le cookie
          405  +		// car chaque vérifieur est à usage unique
          406  +		self::createPermanentSession($row);
   397    407   
   398         -		$db->update('membres_sessions', [
   399         -			'token'     => $token,
   400         -			'expire'    => $expire,
   401         -		], 'selecteur = :selecteur AND id_membre = :id_membre', [
   402         -			'selecteur' => $cookie->selector,
   403         -			'id_membre' => $row->id_membre,
   404         -		]);
   405         -
   406         -		$hash = hash(self::HASH_ALGO, $token . $row->passe);
   407         -		$new_cookie = $cookie->selector . '|' . $hash;
   408         -
   409         -		$options = self::getSessionOptions();
   410         -
   411         -		setcookie(self::PERMANENT_COOKIE_NAME, $new_cookie, $expire->getTimestamp(),
   412         -			$options['cookie_path'], $options['cookie_domain'], $options['cookie_secure'], true);
   413         -
   414         -
   415         -		$this->id = $row->id_membre;
          408  +		$this->id = $row->id;
   416    409   
   417    410   		self::createUserSession($this->id);
   418    411   
   419    412   		return true;
   420    413   	}
   421    414   
   422    415   	public function logout()