Command injection i eval: jak nie wykonać cudzego kodu (Python, PHP, Node)

Mateusz Bogolubow

21-10-2026

(aktualizacja: 21-10-2026)

Jak działa wstrzyknięcie poleceń i dlaczego eval jest groźny: shell=True, shell_exec i exec w Pythonie, PHP i Node oraz bezpieczne odpowiedniki (lista argumentów, JSON, literal_eval).

Command injection i eval: jak nie wykonać cudzego kodu (Python, PHP, Node)
Command injection to atak, w którym dane od użytkownika trafiają do polecenia systemowego (na przykład przez `shell=True`, `shell_exec` albo `exec`) i powłoka wykonuje także to, co atakujący dopisał po średniku. Obrona to uruchamianie programów z listą argumentów zamiast jednego tekstu, a przy `eval` użycie parsera, takiego jak JSON albo `ast.literal_eval`. Zasada jest ta sama co przy SQL injection: nie sklejaj danych użytkownika z kodem.

Wiele aplikacji w pewnym momencie sięga po system: konwertuje obraz, wywołuje git, kompresuje plik albo uruchamia skrypt. Najprostszy sposób to złożyć polecenie jako tekst i przekazać je powłoce. To ta sama pułapka co przy zapytaniach do bazy, tylko zamiast SQL wykonuje się polecenie systemowe albo kod Twojego języka. Poniżej sprawdzam to w Pythonie, PHP i Node.js. Dla bezpieczeństwa w testach używam wyłącznie echo, ls i odczytu bieżącego katalogu na komputerze testowym, a nie niczego, co coś zmienia.

Czym jest command injection i czym różni się od eval?

To dwie pokrewne rzeczy, które łączy jedno: tekst od użytkownika zostaje potraktowany jak instrukcja.

Command injectionCode injection przez eval
Co się wykonujepolecenie w powłoce systemu (bash, sh)kod w języku Twojej aplikacji (Python, PHP, JavaScript)
Typowy punkt wejściashell=True, os.system, shell_exec, execeval, new Function
Skutekdowolne polecenia z uprawnieniami aplikacjidowolny kod z uprawnieniami aplikacji
Obronalista argumentów, brak powłokiparser danych zamiast eval

Podobny mechanizm przy zapytaniach opisuje artykuł o SQL injection, a przy kodzie wstawianym do strony artykuł o XSS.

Jak wygląda wstrzyknięcie polecenia?

Funkcja pozdrawia użytkownika, wywołując echo. Imię pochodzi z formularza. Wersja podatna skleja polecenie w jeden tekst, a wersja bezpieczna podaje program i argumenty osobno:

import subprocess

name = "x; echo WSTRZYKNIETE"

# podatne: całość trafia do powłoki
subprocess.run(f"echo Czesc {name}", shell=True, capture_output=True, text=True).stdout
# 'Czesc x\nWSTRZYKNIETE\n'

# bezpieczne: lista argumentów, bez powłoki
subprocess.run(["echo", "Czesc", name], capture_output=True, text=True).stdout
# 'Czesc x; echo WSTRZYKNIETE\n'
$name = "x; echo WSTRZYKNIETE";

// podatne
shell_exec("echo Czesc " . $name);
// "Czesc x\nWSTRZYKNIETE\n"

// bezpieczne: tablica argumentów w proc_open
$p = proc_open(['echo', 'Czesc', $name], [1 => ['pipe', 'w']], $pipes);
stream_get_contents($pipes[1]);
// "Czesc x; echo WSTRZYKNIETE\n"
proc_close($p);
import { exec, execFile } from 'node:child_process';
import { promisify } from 'node:util';

const name = 'x; echo WSTRZYKNIETE';

// podatne
(await promisify(exec)(`echo Czesc ${name}`)).stdout;
// 'Czesc x\nWSTRZYKNIETE\n'

// bezpieczne
(await promisify(execFile)('echo', ['Czesc', name])).stdout;
// 'Czesc x; echo WSTRZYKNIETE\n'

We wszystkich trzech językach wynik jest taki sam. W wersjach podatnych średnik kończy pierwsze polecenie i powłoka uruchamia drugie, wpisane przez użytkownika, a na wyjściu pojawia się osobna linia WSTRZYKNIETE. W wersjach bezpiecznych cały tekst jest jednym argumentem programu echo, więc znaki specjalne nie mają żadnego znaczenia. W prawdziwym ataku w miejscu echo WSTRZYKNIETE byłoby inne polecenie.

W Pythonie ten sam efekt daje os.system: polecenie z drugim echo po średniku też się wykonało.

Czy shlex.quote i escapeshellarg wystarczą?

Są sposobem na „załatanie” podatnego kodu, bo ujmują tekst w cudzysłowy tak, by powłoka nie interpretowała znaków specjalnych:

import shlex
subprocess.run(f"echo Czesc {shlex.quote(name)}", shell=True, capture_output=True, text=True).stdout
# 'Czesc x; echo WSTRZYKNIETE\n'

W PHP robi to escapeshellarg(). W obu przypadkach w moim teście wynik był poprawny (jedna linia). To jednak rozwiązanie kruche: wystarczy zapomnieć o jednym argumencie, żeby wrócił błąd, a reguły cytowania zależą od powłoki i systemu. Lista argumentów jest po prostu prostsza i nie wymaga pamiętania o niczym. Traktuj cytowanie jako awaryjną łatkę, nie jako wzorzec.

Co to jest wstrzyknięcie opcji i jak się przed nim bronić?

Nawet bez powłoki użytkownik może zaszkodzić, jeśli jego wartość zaczyna się od myślnika. Program potraktuje ją wtedy jako opcję, a nie jako nazwę pliku. Przykład z ls w katalogu z dwoma plikami:

user = "-la"

# wartość użytkownika uznana za opcję: długi format listingu
subprocess.run(["ls", user], capture_output=True, text=True, cwd="dir").stdout

# dwa myślniki kończą opcje: wartość to teraz nazwa pliku (a pliku nie ma)
subprocess.run(["ls", "--", user], capture_output=True, text=True, cwd="dir")
# kod wyjścia 1, pusty stdout

W pierwszym wywołaniu ls wypisał pliki w długim formacie, czyli zadziałał tak, jakby -la wpisał programista. W drugim, z separatorem --, wartość została nazwą pliku i program zgłosił błąd. Separator -- obsługuje wiele programów, ale nie wszystkie (przetestowałem tylko ls), więc sprawdź dokumentację programu, który wywołujesz. Lepszą obroną jest sprawdzenie wartości przed użyciem, na przykład odrzucenie nazw zaczynających się od -.

Dlaczego eval jest groźny?

eval wykonuje tekst jako kod Twojego języka. Poniżej tekst, który wygląda jak dane, ale jest kodem:

eval("__import__('os').getcwd()")   # wykonał kod i zwrócił ścieżkę katalogu
eval('process.cwd()');                    // wykonał kod, zwrócił ścieżkę
new Function('return process.cwd()')();   // to samo
eval("return getcwd();");                 // to samo

Tu zwracam bezpieczny odczyt katalogu, ale w prawdziwym ataku w jego miejscu mogłoby być odczytanie plików lub uruchomienie poleceń. new Function w JavaScripcie jest równie niebezpieczne jak eval, więc nie traktuj go jako obejścia.

Najczęściej eval służy do zamiany tekstu na dane. Do tego istnieją bezpieczne narzędzia:

PotrzebujeszZamiast evalCo się stanie z kodem w tekście
Dane (listy, słowniki, liczby)json.loads, JSON.parse, json_decodebłąd (SyntaxError)
Literały Pythona ([1, 2, {'a': 3}])ast.literal_evalbłąd (ValueError)
Proste wyrażenie z listy dozwolonychwłasny słownik funkcji, np. {"dodaj": dodaj}niedozwolona nazwa jest odrzucana

W teście ast.literal_eval poprawnie wczytał [1, 2, {'a': 3}], ale dla 2 + 2 i __import__('os').getcwd() zgłosił ValueError: przyjmuje wyłącznie literały, nie wyrażenia. JSON.parse('process.cwd()') w Node kończy się SyntaxError, a JSON.parse('[1, 2]') zwraca tablicę.

Gdzie to się zdarza w aplikacjach webowych?

Najczęściej tam, gdzie aplikacja używa danych użytkownika w poleceniu: nazwy pliku z uploadu przekazanej do narzędzia graficznego, adresu podanego przez użytkownika wstawionego do curl, nazwy gałęzi w git. Jeśli Twój Route Handler w Next.js albo endpoint w FastAPI uruchamia polecenie systemowe, obowiązuje dokładnie ta sama zasada: program i argumenty osobno, a wartość od użytkownika najpierw sprawdzona. Przy plikach pomaga zasada z artykułu o uploadzie plików w FastAPI: zapisuj plik pod własną nazwą i właśnie jej używaj w poleceniu, a nie nazwy od klienta. Przykładów w Next.js nie uruchamiałem, testy wykonałem w czystym Node.js.

Jakie błędy mentor poprawia najczęściej?

Uwaga mentora: nazwa pliku od użytkownika to dane z zewnątrz tak samo jak pole formularza. Nie wstawiaj jej do polecenia ani do ścieżki bez sprawdzenia.

Poniżej cała lista pułapek w jednym miejscu:

PułapkaSkutekJak uniknąć
shell=True, shell_exec, exec() z danymi z zewnątrzwykonanie dodatkowych poleceń po średnikulista argumentów (subprocess.run([...]), execFile, proc_open z tablicą)
Poleganie wyłącznie na shlex.quote / escapeshellargwystarczy jedno pominięcielista argumentów, a cytowanie tylko awaryjnie
Wartość zaczynająca się od -potraktowana jak opcja programuseparator -- (jeśli program go obsługuje) lub odrzucenie takich wartości
eval na tekście od użytkownikawykonanie kodu aplikacjiJSON, ast.literal_eval lub słownik dozwolonych funkcji
new Function(...) jako „bezpieczny eval”to samo ryzyko co evalte same alternatywy
Nazwa pliku od klienta w poleceniuwstrzyknięcie polecenia lub opcjiwłasna nazwa pliku wygenerowana przez serwer
Uruchamianie procesu z pełnymi uprawnieniamiskutki ataku są największenajmniejsze uprawnienia, kontener lub osobny użytkownik

Co dalej?

Masz teraz komplet czterech klas problemów, w których dane z zewnątrz stają się kodem lub poleceniem. Kod w przeglądarce opisuje XSS, kod w zapytaniu do bazy SQL injection, a sfałszowane żądania w imieniu użytkownika CSRF. Jeśli chcesz uczyć się budowania bezpiecznych aplikacji z mentorem, który ocenia Twój kod, sprawdź kurs Python backend albo kurs PHP.

Konsultacja i warsztaty devmentor.pl

Masz pytania?