Witaj Gościu! ( Zaloguj | Rejestruj )

Forum PHP.pl

 
Reply to this topicStart new topic
> Zabezpieczenie skryptu PHP
Baku12345
post 4.03.2017, 20:37:48
Post #1





Grupa: Zarejestrowani
Postów: 46
Pomógł: 1
Dołączył: 23.04.2011

Ostrzeżenie: (0%)
-----


Witam
Mam skrypt co do którego mam zastrzeżenia jeśli chodzi o bezpieczeństwo. Nie wrzucę go ze względu na to, że jest zbyt długi. Wrzucę natomiast jego uproszczoną wersję by odnieść się do problemu. A wygląda on tak
  1. <?php
  2.  
  3. $now = time();
  4. $session_data = "";
  5.  
  6. $pdo = new PDO('mysql:host=localhost; port=3306; dbname=test', 'root', '');
  7.  
  8. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
  9. $pdo->exec("SET NAMES 'utf8'");
  10.  
  11. if (isset($_POST['login']) && isset($_POST['password']))
  12. {
  13. if (!empty($_POST['login']) && !empty($_POST['password']))
  14. {
  15. $login = addslashes(strip_tags($_POST['login']));
  16. $password = hash_hmac('sha256', $_POST['password'], '2NYbH5qS8J');
  17.  
  18. $stmt = $pdo->prepare("SELECT * FROM users WHERE login = :login && password = :password");
  19. $stmt -> execute(array(':login' => $login, ':password' => $password));
  20.  
  21. if ($row = $stmt->fetch(PDO::FETCH_ASSOC))
  22. {
  23. $_SESSION[$session_data] = array('id' => $row['id'], 'time' => $now, 'user' => $row['name']);
  24. }
  25. else
  26. {
  27. echo "Podałeś błędny login lub hasło";
  28. }
  29. }
  30. else
  31. {
  32. echo "Nie wprowadziłeś wszystkich danych";
  33. }
  34. }
  35.  
  36. $max_idle_time = 900;
  37. if (isset($_SESSION[$session_data]) && !empty($_SESSION[$session_data]))
  38. {
  39. if ($now - $_SESSION[$session_data]['time'] < $max_idle_time)
  40. {
  41. $_SESSION[$session_data]['time'] = $now;
  42. echo "Jesteś zalogowany";
  43. // tu następuje pobranie z bazy danych danych przeznaczonych dla użytkownika o danym id a następnie ich wyświetlenie
  44. }
  45. else
  46. {
  47. echo "Przekroczono czas bezczynności";
  48. }
  49. }
  50. else
  51. {
  52. echo '<h1>Panel Użytkownika</h1>
  53. <form method="post" action="">
  54. <div>
  55. <input type="text" name="login" placeholder="Twój login" value="">
  56. </div>
  57. <div>
  58. <input type="password" name="password" placeholder="Twoje hasło" value="">
  59. </div>
  60. <button type="submit">Zaloguj się</button>
  61. </form>';
  62. }
  63. ?>


Skrypt działa dobrze. Przy pierwszym wejściu na stronę przechodzimy na koniec kodu do formularza, ponieważ zmienna login, password i session nie istnieją.
Jeżeli wyślemy login, hasło i skrypt sprawdzi, że nie są puste to jest porównywany hash z tym znajdującym się w bazie. Dalej jeśli się zgadzają jest tworzona zmienna sesyjna, w której jest id, czas i nazwa użytkownika. W drugiej części skrypt sprawdza, że istnieje sesja między innymi z id użytkownika i wyświetla dane przeznaczone tylko dla niego.

Problem w tym, że jeżeli nie znam loginu ani hasła użytkownika to też mogę się dostać do danych przeznaczonych tylko dla danego użytkownika. Wystarczy, że w sposób "sztuczny" utworzę zmienną sesyjną z id i zmieniając tylko id mogę sobie skakać z konta na konto.

Pytanie jak zabezpieczyć taki skrypt?

PS: Trafiłem przypadkiem na ten błąd robiąc kopię tego skryptu, ale z podpiętą inną bazą innym loginem i hasłem. Odpaliłem jeden skrypt podałem login i hasło, a potem nie zamykając przeglądarki odpaliłem drugi skrypt z całkiem inną bazą loginem i hasłem w niej (tylko id użytkownika się zgadzało) i byłem zalogowany na obu skryptach. Mogłem sobie zmieniać id użytkownika w jednym skrypcie, a w drugim byłem zalogowany na tym użytkowniku o danym id. Wnioskuję więc, że każdy mógłby wysłać zmienną sesyjną z id i by został wpuszczony i by mu się wyświetliły dane tego użytkownika.

PS2: Po linii 39 następuje sprawdzenie czy została utworzona zmienna sesyjna między innymi z id użytkownika, a w linii 46 jest select który pobiera dane przeznaczone dla użytkownika o tym id. I właśnie tu jest luka, którą nie wiem jak załatać, jedyne co mi przychodzi do głowy to ponowne sprawdzenie czy nazwa użytkownika i hasło się zgadzają, ale wtedy jaki sens ma kod przed linią 39? Mam nadzieję, że jasno to opisałem smile.gif W skrócie macie 2 kopie tego skryptu logujecie się na jednej to w drugiej jesteście zalogowani mimo, że hasła użytkownika w bazie podpiętej do drugiego skrypcie nie znacie. Prościej nie umiem smile.gif

Ten post edytował Baku12345 4.03.2017, 21:40:30
Go to the top of the page
+Quote Post
Damonsson
post 4.03.2017, 22:30:02
Post #2





Grupa: Zarejestrowani
Postów: 2 355
Pomógł: 533
Dołączył: 15.01.2010
Skąd: Bydgoszcz

Ostrzeżenie: (0%)
-----


Przypisuj cookie do konkretnej domeny, tyle. A jak masz separację pod tą samą domeną, to dodaj do cookie adres i sprawdzaj czy się zgadza. Zapewni to separacje pomiędzy projektami. Zwiększy jakoś tam bezpieczeństwo, że pomiędzy skryptami nie będzie można edytować sesji.

Ale oczywiście nikt nie może zmienić sobie ID, bo jest pobierane z bazy danych. Pytanie w stylu: "Mam hasło do mojej witryny i mogę się na nią włamać dzięki temu. Jak zabezpieczyć skrypt?"

Ten post edytował Damonsson 4.03.2017, 22:34:14
Go to the top of the page
+Quote Post
JakubBab
post 4.03.2017, 22:38:09
Post #3





Grupa: Zarejestrowani
Postów: 96
Pomógł: 2
Dołączył: 13.07.2015

Ostrzeżenie: (0%)
-----


Co masz na myśli pisząc sztucznie? Ktoś musiałby się dobrać do Twojego kompa żeby ukraść to id (albo do konta użytkownika który korzysta z Twojej ulsugi i podrkaść wartość sesyjną). Może to zrobić np. korzystając z public network a jednym z użytkowników jest niegrzeczny Janusz który nasłuchuje na Twój traffic i kradnie info! evil bitch!

Jak się zabezpieczyć? Ciężko znaleść złoty środek ale możesz spróować poprzez sprwadzanie IP naprzykład.


Takie masz sessions data? smile.gif

  1. $_SESSION[$session_data] = array('id' => $row['id'], 'time' => $now, 'user' => $row['name']);


Wez sobie to md5 sformatuj lub crypt(), a później porównuj np:

http://php.net/hash_equals

Po drugie:

Nie wszystko w jednym pliku. Trzymasz połączenie z bazą, logikę i prezentacje danych w jednym miejscu. Właśnie uśmierciłeś małęgo pingwinka.

Tak naprawde wystarczy użyć empty() , bez isset()
Go to the top of the page
+Quote Post
Baku12345
post 5.03.2017, 03:47:07
Post #4





Grupa: Zarejestrowani
Postów: 46
Pomógł: 1
Dołączył: 23.04.2011

Ostrzeżenie: (0%)
-----


W sumie faktycznie dodanie adresu ip lub domeny mogłoby trochę pomóc. Co do hashowania tej linii
  1. $_SESSION[$session_data] = array('id' => $row['id'], 'time' => $now, 'user' => $row['name']);

to byłby to dobry pomysł, gdybym w linii 46 nie pobierał danych po id użytkownika
  1. $stmt = $pdo->prepare("SELECT * FROM tabela WHERE user_id = :user_id ORDER BY ... ASC");
  2. $stmt->execute(array(':user_id' => $_SESSION[$session_data]['id']));

Tak więc muszę mieć tu czyste id bez hashowania.

Co do pytania co mam na myśli pisząc sztucznie to mam na myśli stworzenie sesji przed wejściem na stronę. Wystarczy, że stworzę sobie plik atak.php i umieszczę w nim to
  1. <?php
  2.  
  3. $now = time();
  4. $session_data = "";
  5.  
  6. $_SESSION[$session_data] = array('id' => 1, 'time' => $now);
  7. ?>

Jeśli go otworzę w przeglądarce, to stworzy się sesja z id użytkownika 1 i aktualnym czasem. Teraz jak otworzę plik index.php (ten co go pokazałem w pierwszym poście, to nawet się nie będę musiał logować na użytkownika o id 1. Będzie zalogowany bez podawania loginu i hasła smile.gif I tu jest problem.

Ten post edytował Baku12345 5.03.2017, 03:51:49
Go to the top of the page
+Quote Post
viking
post 5.03.2017, 06:22:38
Post #5





Grupa: Zarejestrowani
Postów: 6 366
Pomógł: 1115
Dołączył: 30.08.2006

Ostrzeżenie: (0%)
-----


A teraz się zastanów ile jest możliwych kombinacji przy tej ilości znaków nazwy sesji.
Oczywiście mógłby dojść do powtórzenia ale szanse na to nikłe.
Po co to addslashes? Raczej fitruj poprzez sprawdzanie dopuszczalnych znaków.


--------------------
Go to the top of the page
+Quote Post
daro0
post 5.03.2017, 10:25:47
Post #6





Grupa: Zarejestrowani
Postów: 88
Pomógł: 12
Dołączył: 17.09.2014
Skąd: Krasnystaw

Ostrzeżenie: (0%)
-----


Postaram się tutaj przedstawić to jakoś w miarę klarownie, bo na podstawie wielu postów na różnych forach widać jak na dłoni, że jak się ktoś czegoś uczy to jeszcze w starym stylu i pewnie też z jakichś beznadziejnych i niepraktycznych tutoriali PHP które są jeszcze w internecie.

Po pierwsze, podział kodu i podział odpowiedzialności, dla uproszczenia zakładam że wszystkie pliki PHP są w jednym katalogu projektu, lecę więc po kolei, od helperów, przez szablony aż po inne pliki. Uwaga, zakładam że liczą się duże i małe litery w nazwach plików z klasami.

Klasa View.php odpowiedzialna za renderowanie szablonów:

  1. <?php
  2.  
  3. defined('DOCROOT') OR die('No direct script access.');
  4.  
  5. class View
  6. {
  7. private $_file;
  8. private $_data;
  9.  
  10. public static function factory($file, array $data)
  11. {
  12. return new View($file, $data);
  13. }
  14.  
  15. public function __construct($file, array $data)
  16. {
  17. $this->_file = DOCROOT . $file . '.php';
  18. $this->_data = $data;
  19. }
  20.  
  21. public function render()
  22. {
  23. extract($this->_data);
  24. include $this->_file;
  25. return ob_get_clean();
  26. }
  27.  
  28. public function __toString()
  29. {
  30. return $this->render();
  31. }
  32.  
  33. }


Klasa Auth.php odpowiedzialna za logowanie i sprawdzanie czy użytkownik jest zalogowany:

  1. <?php
  2.  
  3. defined('DOCROOT') OR die('No direct script access.');
  4.  
  5. class Auth
  6. {
  7. private $_config;
  8. private $_db;
  9. private $_session_key;
  10.  
  11. public function __construct($config)
  12. {
  13. $this->_config = $config;
  14. $dsn = 'mysql:host=' . $config['database']['host'] . '; port='.$config['database']['port'].'; dbname=' . $config['database']['database'];
  15. $this->_db = $pdo = new PDO($dsn, $config['database']['username'], $config['database']['password']);
  16. $this->_session_key = $config['auth']['session_key'];
  17. }
  18.  
  19. public function get_user()
  20. {
  21. $now = time();
  22. $max_idle_time = $this->_config['auth']['max_idle_time'];
  23.  
  24. if (array_key_exists($this->_session_key, $_SESSION))
  25. {
  26. if (($now - $_SESSION[$this->_session_key]['last_active'] < $max_idle_time))
  27. {
  28. return $_SESSION[$this->_session_key]['user'];
  29. }
  30. return NULL;
  31. }
  32.  
  33. return NULL;
  34. }
  35.  
  36. public function logged_in()
  37. {
  38. return ($this->get_user() !== NULL);
  39. }
  40.  
  41. public function login($username, $password)
  42. {
  43. $password = hash_hmac('sha256', $password, $this->_config['auth']['hash_key']);
  44. $stmt = $this->_db->prepare('SELECT * FROM users WHERE username = :username AND password = :password');
  45. $stmt->execute(array(':username' => $username, ':password' => $password));
  46.  
  47. if ($row = $stmt->fetch(PDO::FETCH_ASSOC))
  48. {
  49. $_SESSION[$this->_session_key] = array(
  50. 'user' => $row,
  51. 'last_active' => time(),
  52. );
  53.  
  54. return true;
  55. }
  56. return false;
  57. }
  58. }


plik config.php z danymi do połączenia z bazą i danymi do obsługi logowania:

  1. <?php
  2.  
  3. return array(
  4. 'database' => array(
  5. 'host' => 'localhost',
  6. 'port' => 3306,
  7. 'database' => 'testdb',
  8. 'username' => 'root',
  9. 'password' => 'root',
  10. ),
  11. 'auth' => array(
  12. 'session_key' => 'auth_user',
  13. 'hash_key' => '213dsklsdks32000',
  14. 'max_idle_time' => 60,
  15. ),
  16. );


główny layout, plik layout.php

  1. <html lang="pl">
  2. <head>
  3. <meta charset="utf-8" />
  4. </head>
  5. <body>
  6. <?php if (isset($content)) echo $content; ?>
  7. </body>
  8. </html>


szablon logowania login.php

  1. <?php if ($user): ?>
  2. <H1><?= 'Jesteś zalogowany(a) jako: ' . $user['username'] ?></H1>
  3. <script>
  4. function reloadAsGet()
  5. {
  6. var loc = window.location;
  7. window.location = loc.protocol + '//' + loc.host + loc.pathname + loc.search;
  8. }
  9.  
  10. setTimeout(function(){
  11. reloadAsGet();
  12. }, <?= $reload_timeout * 1000 ?>);
  13. </script>
  14. <?php else: ?>
  15. <h1>Logowanie</h1>
  16. <form action="" method="POST">
  17. <div>
  18. <input type="text" name="username" value="">
  19. </div>
  20. <div>
  21. <input type="password" name="password" value="">
  22. </div>
  23. <button type="submit">Zaloguj</button>
  24. </form>
  25. <?php endif; ?>
  26. <h3 id="message"><?= $message ?></h3>


no i ostatecznie index.php

  1. <?php
  2. define('DOCROOT', realpath(dirname(__FILE__)).DIRECTORY_SEPARATOR);
  3.  
  4. spl_autoload_extensions('.php');
  5. spl_autoload_register();
  6.  
  7. $config = include 'config.php';
  8. $auth = new Auth($config);
  9. $message = '';
  10. $reload_timeout = $config['auth']['max_idle_time'];
  11.  
  12. if ($_SERVER['REQUEST_METHOD'] === 'POST')
  13. {
  14. $result = $auth->login($_POST['username'], $_POST['password']);
  15. if (!$result)
  16. {
  17. $message = 'Błąd logowania';
  18. }
  19. }
  20.  
  21. $user = $auth->get_user();
  22.  
  23. $content = View::factory('login', compact('user', 'message', 'reload_timeout'));
  24. $layout = View::factory('layout', compact('content'));
  25.  
  26. echo $layout;


Co też testowałem na jednej ze swoich baz MySQL na localhoście, nie wiem tylko czy ta funkcja reloadAsGet zadziała na wszystkich przeglądarkach. W tym przykładzie po ustawionym czasie bezczynności 1 minuty po zalogowaniu nastąpi po tym czasie przeładowanie strony nie jako POST ale GET ze sprawdzeniem czy użytkownik jest zalogowany, jeśli nie to się wyświetli ten formularz.

Po drugie. Jest chyba dość cienka granica między bezpieczeństwem a paranoją. Tu zakładam że w każdym innym projekcie są różne te hash_key i zakładam że jest SHA256 i 64 znaków w polach z hasłem. Ten automatyczny reload to w pewnych przypadkach może pewnie nawet przeszkadzać niż pomagać, w szczególności może denerwować.

I nie jakieś przypisywanie bezpośrednio do sesji a tutaj użyty jest klucz auth_user związany z tą właśnie obsługą logowania a ze względów praktycznych do tejże sesji zapisywany jest cały wiersz z bazy danych a nie tylko niektóre pola, nie muszę chyba tłumaczyć dlaczego.
Go to the top of the page
+Quote Post

Reply to this topicStart new topic
1 Użytkowników czyta ten temat (1 Gości i 0 Anonimowych użytkowników)
0 Zarejestrowanych:

 



RSS Wersja Lo-Fi Aktualny czas: 23.05.2024 - 19:18