(W miarę) bezpieczne uruchamianie skryptów PHP poprzez shell_exec()

Zgod­nie z obiet­nicą dzisiaj część doty­cząca bezpiecz­nego urucha­mia­nia zewnętrz­nych skryp­tów PHP. Od razu mówię, że nie jestem jakimś guru dot. zabez­pie­czeń syste­mów Unix. Zebra­łem rozsy­pane po inter­ne­cie infor­ma­cje i spró­bo­wa­łem złożyć to w całość.

Gist został uaktu­al­niony o klasę ICMPPingProcesserrzuć­cie okiem. Posłuży jako baza do naszych działań.

Jak już wspo­mnia­łem w poprzed­nim wpisie, w Linuk­sie nie można wyko­nać pole­ce­nia socket_create() na typo­wym użyt­kow­niku Apache, czyli www-data. Aby obejść ten problem skorzy­sta­łem z pole­ce­nia posix_seteuid(0) zmie­nia­ją­cego użyt­kow­nika procesu na roota na czas życia obiektu ICMPPing. Z kolei aby wyko­nać to pole­ce­nie musimy stać się rootem :-). Z termi­nala nic trudnego:

sudo php icmp.php http://spiechu.pl

Zosta­niemy zapy­tani o hasło i dosta­jemy odpo­wiedź skryptu. Problem poja­wia się gdy chcemy wywo­łać pole­ce­nie z poziomu innego skryptu PHP. W Ubuntu nie da się/nie umiem (a próbo­wa­łem na wiele sposo­bów) wpisać hasła dla sudo z poziomu lini komend. Wobec tego trzeba zmusić sudo aby nie pytał o hasło. Oczy­wi­ście opcja aby nadać użyt­kow­ni­kowi www-data wszyst­kie upraw­nie­nia roota na stałe nie wcho­dzi w grę. Trzeba maksy­mal­nie zawę­zić „pole manewru” dla www-data.

Wobec tego przy­cze­piamy się do pliku /etc/sudoers, który prze­cho­wuje upraw­nie­nia zwią­zane z pole­ce­niem sudo. Przy­po­mi­nam, że plik sudo­ers należy edyto­wać wyłącz­nie za pomocą pole­ce­nia visudo, a więc w terminalu:

sudo visudo

i jedziemy z edycją:

# Ustawiamy kilka aliasow
# gdy skryptow zrobi sie sporo
# bedzie latwiej zarzadzac tym balaganem
User_Alias APACHE_USER = www-data
Runas_Alias ROOT_USER = root
Host_Alias PHP_MACHINE = dave-ubunciak
Cmnd_Alias ICMP_SCRIPT = /usr/bin/php /home/dave/icmp.php http\://*
 
# Wlasciwe polecenie
APACHE_USER PHP_MACHINE = (ROOT_USER) NOPASSWD: ICMP_SCRIPT

Pole­ce­nie można prze­tłu­ma­czyć tak:
Użyt­kow­nik www-data na maszy­nie dave-ubunciak jako root może wyko­nać skrypt php o nazwie icmp.php bez hasła z para­me­trem rozpo­czy­na­ją­cym się od http://. Wszel­kie odstęp­stwa będą trak­to­wane komu­ni­ka­tem sudo: no tty present and no askpass program speci­fied Sorry, try again.
Od tego momentu możemy w dowol­nym skryp­cie wywoływać:

// na czas developmentu warto na koncu polecenia wpisac
// 2>&1 dzieki czemu strumien stderr przekierujemy na wyjscie
$process = shell_exec('sudo php /home/user/icmp.php http://www.spiechu.pl');
echo $process;

Co jesz­cze można zrobić z samym plikiem icmp.php? Możemy zmie­nić mu użyt­kow­nika i grupę na root i ogra­ni­czyć możli­wość jego wyko­na­nia wyłącz­nie do roota, czyli:

sudo chown root icmp.php
sudo chgrp root icmp.php
sudo chmod 400 icmp.php

Na koniec przy­cze­pię się do samego icmp.php. Napi­sa­łem klasę ICMPPingProcesser, która spraw­dza czy podany adres www jest prawi­dłowy i odsiewa wszystko poza podsta­wo­wym adre­sem hosta. Poni­żej przy­ta­czam w całości:

class ICMPPingProcesser
{
    /**
     * @var string
     */
    protected $urlAddress;
 
    /**
     * @param string $urlAddress
     */
    public function __construct($urlAddress)
    {
        $this->urlAddress = $urlAddress;
    }
 
    /**
     * Returns 'Trying {$urlAddress}: PING RESPONSE: Everything OK'
     * when url address replied correctly
     *
     * @return string
     */
    public function ping()
    {
        try {
            $message = '';
            $urlToPing = $this->processUrl($this->urlAddress);
            if (!$this->isUrlExists($urlToPing)) {
                throw new Exception("{$urlToPing} doesn't exist!");
            }
            $icmp = new ICMPPing();
            $respond = $icmp->sendPacket($urlToPing, 'Everything OK');
            $message = "Trying {$urlToPing}: ";
            $message .= "PING RESPONSE: {$icmp->analyzeRespond($respond)}";
 
            return $message;
        } catch (Exception $e) {
            $message .= $e->getMessage();
 
            return $message;
        }
    }
 
    /**
     * Returns sanitized url host from param
     *
     * @param  string    $urlAddress
     * @return string    url host
     * @throws Exception when url address is not valid
     */
    protected function processUrl($urlAddress)
    {
        $sanitizedUrl = filter_var($urlAddress, FILTER_SANITIZE_URL);
        if ($sanitizedUrl === false || filter_var($sanitizedUrl, FILTER_VALIDATE_URL) === false) {
            throw new Exception("{$urlAddress} is not valid URL");
        }
 
        return parse_url($sanitizedUrl, PHP_URL_HOST);
    }
 
    /**
     * Checks if url address exists
     *
     * @param  string  $urlAddress
     * @return boolean
     */
    protected function isUrlExists($urlAddress)
    {
        if (gethostbyname($urlAddress) === $urlAddress) {
            return false;
        }
 
        return true;
    }
 
}
 
if (PHP_SAPI === 'cli' && isset($_SERVER['argv'][1])) {
    $pingProcesser = new ICMPPingProcesser($_SERVER['argv'][1]);
    echo $pingProcesser->ping();
} else {
    echo 'Not in cli mode or agument not set';
}

Ostat­nie kilka lini­jek spraw­dza czy skrypt odpa­lany jest w trybie lini komend i czy istnieje jakiś argument.

Reasu­mu­jąc:

  1. Właści­cie­lem pliku powi­nien być root i tylko root powi­nien mieć prawo odczy­tać zawartość.
  2. W /etc/sudoers prawo wyko­na­nia skryptu powinno być maksy­mal­nie zawę­żone (żadnych ALL).
  3. Skrypt przed wyko­na­niem powi­nien dokład­nie spraw­dzać podane argumenty.

Podobne wpisy:

  1. PDO poprzez Depen­dency Injec­tion Conta­iner [cz. 2/2]
  2. Tworze­nie pakie­tów ICMPPHP
  3. PDO poprzez Depen­dency Injec­tion Conta­iner [cz. 1/2]
  4. 2 sposoby na wyłą­cze­nie kompu­tera o okre­ślo­nej godzinie

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.

Możesz użyć następujących tagów oraz atrybutów HTML-a: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <p> <pre lang="" line="" escaped=""> <q cite=""> <strike> <strong>