Pomoc - Szukaj - Użytkownicy - Kalendarz
Pełna wersja: Super eval()
Forum PHP.pl > Forum > PHP
pp-layouts
Początkowo zadałem pytanie jak rozwiązać problem, i w jakimś amoku sam go rozwiązałem.

Problem jest stary jak PHP. Powiedzmy, że chcemy wykonać wygenerowany kod via eval(), ale nie mamy pewności, że nie spowoduje on błędu fatalnego - kładąc główny kod. Po co? Ja na przykład zrobiłem kompilator własnego języka. Nie będę się już wdawał w szczeóły po co. Był potrzebny i już. Klient zamówił - klient dostał. Problem jednak w tym, że klient skrypty w tym nowym języku musi dość często aktualizować. W dodatku klient nie jest programistą. Dlatego język skryptu maksymalnie przypomina naturalny język polski. Nie udało się jednak uciec przed jakąś podstawową składnią, w końcu to też język programowania, który w dodatku robi dość skomplikowane operacje na danych. Jeśli w skrypcie użytkownika pojawi się błąd, którego nie wykryje mój kompilator - powstanie błędny kod wynikowy, który wysypie kod główny. A to się po prostu nie ma prawa zdarzyć. Klient musi otrzymać informację, że ma w skrypcie błąd, dostać jakąś podpowiedź w postaci np fragmentu kodu, którego nie udało się wykonać. Ważne, żeby dostał informację KTÓRY z kilkunastu skryptów się wysypał. Pozostałe MUSZĄ się wykonać. Używając eval() - niewykonalne.

Prosty test: @eval('return test::test();'); Jeśli nie ma klasy test - kod się sypnie. Nic po eval() się już nie wykona. Bo jak czytałem na StackOverflow - błędy fatalne są fatalne.

Dobra, to teraz rozwiązanie, czyli SUPER EVAL smile.gif

  1. function s_eval($code) {
  2. exec('php -r ' . escapeshellarg("function ___() { $code};echo serialize(___());"), $output, $retval);
  3. return $retval ? $output[1] : unserialize($output[0]);
  4. }
Uwaga: żeby nie było, poprawiłem tą funkcję - dodałem escapeshellarg, inaczej kod by się wysypał od pojedynczego quota.


Funkcja zachowuje się jak zwykły eval() z jednym wyjątkiem: w przypadku błędu fatalnego zwraca komunikat o błędzie jako string. Oczywiście można ją przerobić, żeby zwracała informację o tym, czy wystąpił błąd, i wartość wyjściową, to już kwestia trywialna. Oczywiście pewnie jest to wolniejsze od zwykłego evala, ale jest jedynym sposobem na uruchomienie kodu mogącego zawierać błąd.

Co istotne, w razie błędu zwracany jest właściwy numer linii w której wystąpił. Nie ma żadnych ograniczeń jeśli chodzi o używanie include, require i klas wewnątrz kodu.

Przyda się komuś?
wookieb
Bardzo bardzo początkującym smile.gif a dlaczego? Bo nikt profesjonalny i przy zdrowych zmysłach nie używa eval tongue.gif aczkolwiek przyznam że pomysł ciekawy lecz nie wszędzie możliwy do zrealizowania.
phpion
Nie prościej zrobić przycisk pod czas edycji "Uruchom kod", który otworzy np. popup ze skryptem, który wykona wpisany kod. Będziesz od razu widział czy działa czy nie.
pp-layouts
Gdyby to się dało zrobić bez evala, to bym zrobił. Dwa, że ten uruchamiany kod, to jest kilkanaście skryptów, które się muszą wszystkie uruchomić, ewentualnie może się pokazać w którym jest błąd. A aplikacja nie służy ani do debugowania tych skryptów, ani do ich pisania. Uruchamianie ich o jest jej mała cząstka, w tym czasie AJAX pokazuję 'Proszę czekać' jak się uruchamiają smile.gif No i to "proszę czekać" zalegało na wieczność, jak się któryś ze skryptów i to przy specyficznych danych wejściowych wysypywał. A teraz, w sytuacji awaryjnej pokaże się tylko, że skrypt nr 8 ma błąd, reszta się wykona, aplikacja zwróci wynik, będzie po prostu niekompletny / mniej dokładny.

Co do bardzo początkujących - im bym właśnie najbardziej odradzał użycie evala jakiegokolwiek! Eval, nawet zwykły, jest dość ciężki w użyciu, a użyty niewłaściwie może być niebezpieczny. 99% rzeczy, które kiedyś robiliśmy z evalem da się zrobić bez, lepiej, szybciej i prościej. Ale zostaje ten 1%, jak ten kompilator, gdzie po prostu bez evala byłaby masakra.

erix
Bez chroot i uruchomienia z minimalnymi uprawnieniami nie ma mowy. I tak da się narobić "kuku". ;]

Trzeba wykorzystywać rozwiązania rodem ze SPOJ.
pp-layouts
Ta aplikacja nie jest dostępna publicznie, tylko dla pracowników firmy. Mogą tylko sobie kuku zrobić smile.gif Swoją drogą miałem jakiegoś buga w kompilatorze, wróciłem go do nieco starszej wersji (w której nie zdążyłem jeszcze namieszać po zarwanej nocce) - i nie jestem w stanie napisać skryptu który wywołałby fatala w kodzie wynikowym smile.gif Wszystkie celowo umieszczane bugi wyłapywał albo kompilator, albo parsekit. Tak czy siak, mojego super evala można potraktować jako ciekawostkę i pewien sposób rozszerzenia standardowej funkcjonalności PHP.

Co do robienia kuku, ciekawi mnie jak, przy maksimum złej woli nabroić cokolwiek, jeśli cały kod użytkownika jest kompilowany, czyli jedyne co z niego przechodzi w postaci niezmienionej są stałe. Nie będę zdradzał tutaj całego algorytmu, ale jedynym sposobem na zepsucie czegoś to zepsucie escapowania stringów, żeby kompilator wrzucił cytat jako kod, ale to się DA zabezpieczyć, szczególnie że format skryptu wejściowego jest raczej dość ścisły. Jest tylko jeden rodzaj cytowania (single quote), nie ma możliwości bezpośredniego escapowania w literalach. Jedyny sposób żeby ten znak umieścić w danych wejściowych to użycie wstawki CSV (jest częścią składni mojego języka). Każde inne użycie single quota spowoduje, że kompilator odrzuci kod i zwróci syntax error. A CSV jest zawsze wstawiany jako dane, nie ma możliwości żeby cokolwiek przeszło do kodu.

Wiem, zdolny hacker złamie wszystko, no i właśnie, nie ma co się szczypać - mój eval nie jest mniej bezpieczny od całej reszty kodu czy samego PHP.



  1. /**
  2.  * Performs bullet-proof eval function via separate PHP process
  3.  * NO TEXT OUTPUT ALLOWED IN EVAL'D CODE! ANY TEXT OUTPUT WILL CAUSE AN EXCEPTION!
  4.  * NO UNPROCESSED USER INPUT SHOULD BE ALLOWED IN EVALE'D CODE FOR SECURITY REASONS!
  5.  * FOR DEBUGGED AND SANITIZED INPUT USE STANDARD eval() FOR SPEED
  6.  * @param string $code
  7.  * @return mixed
  8.  */
  9. function s_eval($code) {
  10. exec('php -r ' . escapeshellarg('function ___(){try{'.$code.'}catch(Exception $e){echo serialize($e);}}echo serialize(___());'), $output, $retval);
  11. if ($retval) throw new Exception($output[1] ? $output[1] : "Unexpected shell exit code: $retval");
  12. else {
  13. $result = unserialize($output[0]);
  14. if ($result instanceof Exception) throw $result;
  15. elseif ($result) return $result;
  16. elseif ($output[0] !== false)
  17. throw new Exception('Output detected in evale\'d code while not allowed here: ' . $output[0]);
  18. else return false;
  19. }
  20. }


Finalna, przetestowana wersja tej funkcji. Wszystkie błędy fatalne i nieobsłużone wyjątki wykonywanego kodu zostaną przekazane jako wyjątek, który można, a nawet trzeba obsłużyć w bloku try / catch. Przy okazji warto zauważyć kilka rzeczy, których odkrycie kosztowało mnie kilka zarwanych nocy: maksymalny rozmiar wykonywanego kodu to ca 64kb lub mniej, zależnie od shella, co jest spowodowane maksymalną długością przetwarzanego polecenia. Obejściem tego limitu jest użycie include / require w wykonywanym kodzie. Kolejną różnicą od zwykłego evala, co jest raczej oczywiste - s_eval działa w czystym środowisku, wszelkie wymagania w postaci bibliotek muszą być dołączone jeszcze raz. Przy okazji wyszedł też bug w samym PHP - w shellu nie działa polecenie set_exception_handler(); Obszedłem to poprzez użycie bloku try / catch i przekazanie obiektu wyjątku.

W praktycznym zastosowaniu uruchomienie kodu przebiega w 2 etapach. Pierwszy używa s_eval() i sprawdza, czy kod jest poprawny (względnie loguje wszystkie błędy i wyjątki). Jeśli nie wykryto błędu, kod zapisywany jest w pliku cache i przy kolejnych wywołaniach jest wykonywany zwykłym evalem, co oszczędza ponad 90% czasu. Każde wykrycie błędu w kodzie powoduje inwalidację cache (data pliku cache jest cofana względem daty źródła).  Metoda ta jest także szybsza od użycia parsekita przy każdym uruchomieniu kodu użytkownika (około 8x).

Konieczność wykonania kodu użytkownika wbrew temu, co większość z Was pisze może wystąpić i nie musi to oznaczać wcale błędnych założeń. Eval był i jest ogólnie odradzany przez m. in. olbrzymie trudności w debugowaniu kodu. Z moją funkcją obsługującą wyjątki debugowanie jest prostsze, ba - w ogóle jest możliwe.

Może oszczędzę komuś wielu godzin frustrującego rozgryzania tematu.

To jest wersja lo-fi głównej zawartości. Aby zobaczyć pełną wersję z większą zawartością, obrazkami i formatowaniem proszę kliknij tutaj.
Invision Power Board © 2001-2025 Invision Power Services, Inc.