Předání formulářových polí

Školení, která pořádám

Pokud v aplikaci máme nasazené automatické odhlašování uživatelů, tak se může stát, že k odhlášení dojde v době mezi stažením a odeslání formuláře. Slušností je do přihlašovacího formuláře doplnit do skrytých polí data, která uživatel vyplnil, aby je nemusel zadávat znovu. Pro skalární data to je triviální, pokud však formulář může obsahovat pole (případně víceúrovňová), dá se použít následující řešení:

<?php
$process = $_POST;
while (list($key, $val) = each($process)) {
    if (is_array($val)) {
        foreach ($val as $k => $v) {
            $process[$key . "[$k]"] = $v;
        }
    } else {
        echo '<input type="hidden" name="' . htmlspecialchars($key) . '" value="' . htmlspecialchars($val) . '" />' . "\n";
    }
}
?>

Podobně jako u vypnutí magic_quotes_gpc se nepoužívá rekurze, aby se zabránilo útočníkovi její přílišnou hloubkou shodit PHP. Tento kód je možné vložit do přihlašovacího formuláře.

Pokud formulář obsahoval i vložené soubory, je možné do skrytých polí vložit i jejich původní jméno, typ a obsah, tím se z nich ale stanou běžná POST data. Aby aplikace mohla transparentně pracovat s polem $_FILES, musí přihlašovací skript data do tohoto pole opět vložit:

<?php
// zakódování
foreach ($_FILES as $key => $val) {
    if ($val['error'] == UPLOAD_ERR_OK) {
        echo '<input type="hidden" name="_files[' . htmlspecialchars($key) . '][name]" value="' . htmlspecialchars($val['name']) . '" />' . "\n";
        echo '<input type="hidden" name="_files[' . htmlspecialchars($key) . '][type]" value="' . htmlspecialchars($val['type']) . '" />' . "\n";
        echo '<input type="hidden" name="_files[' . htmlspecialchars($key) . '][data]" value="' . base64_encode(file_get_contents($val['tmp_name'])) . '" />' . "\n";
    }
}

// rozkódování
foreach ($_POST['_files'] as $key => $val) {
    $data = base64_decode($val['data']);
    $_FILES[$key] = array(
        'name' => $val['name'],
        'type' => $val['type'],
        'size' => strlen($data),
        'tmp_name' => tempnam($_ENV["TEMP"], 'file'),
        'error' => UPLOAD_ERR_OK,
    );
    file_put_contents($_FILES[$key]['tmp_name'], $data);
}
?>

Vzhledem ke značné režii (soubor se musí přenášet zpět k uživateli, navíc zakódovaný) bych toto řešení spíše nedoporučoval, velikost zakódovaných dat může navíc překročit post_max_size. Alternativně je možné soubor uložit do dočasného souboru na serveru a jeho jméno dát do formuláře a session proměnné – do session proměnné kvůli tomu, aby si uživatelé soubory nemohli vzájemně krást, do formuláře proto, aby byl ošetřen případ, kdy uživatel odešle víc formulářů v různých oknech. V adresáři, do kterého se soubory ukládají, je vhodné čas od času smazat staré soubory kvůli formulářům, které už nikdy nebyly odeslány. Pokud byste se pro jeden z těchto způsobů rozhodli, je potřeba mít na paměti rozličná omezení: jednak do adresáře $_ENV["TEMP"] nemusí mít skript právo zápisu a hlavně takto vytvořený soubor nepůjde zpracovat funkcí move_uploaded_file.

Lepší je proto možná požádat uživatele o nové nahrání souborů:

<?php
if ($_FILES) {
    echo "<fieldset><legend>Nahrajte prosím znovu tyto soubory:</legend>\n";
    foreach ($_FILES as $key => $val) {
        echo htmlspecialchars($val['name']) . ' (' . round($val['size'] / 1024) . ' KiB): ';
        echo '<input type="file" name="' . htmlspecialchars($key) . '" />' . "\n";
    }
    echo "</fieldset>\n";
}
?>

Všechna řešení by také bylo vhodné rozšířit o zpracování chybových stavů (u posledního způsobu se např. soubory, které nebyly vůbec nahrané, mohou skrýt ve stylu) a pamatovat na to, že i soubory mohou být nahrávané do pole ve tvaru $_FILES["soubor"]["tmp_name"] = array().

Jakub Vrána, Výuka, 15.5.2006, diskuse: 7 (nové: 0)

Diskuse

ATom:

No, po prvním přečtení mi nebylo vůbec jasné, co má tenhle článek za význam. Po druhém přečtení jsem pochopil, že jde asi o to, aby se odeslaná data z formuláře přidala k následující formuláři, který uživatele vyzve k zadání hesla. Protože jinak v tom význam nevidím. Já bych teda ta data prostě uložil do SESSION a vyzval uživatele k opětovnému přihlášení, po přihlášení by se odložená data zpracovala.

Jednou jsem tento problém vyřešil tak, že jsem na stránku, kde byl formulář, který se vyplňoval opravdu dlouho, dal na pozadí JavaScript, který pravidelně ze serveru něco načítal. Tam byl timeout pro session nastavený přímo v konfiguraci PHP.

ikona dgx:

> ...se nepoužívá rekurze, aby se zabránilo útočníkovi její přílišnou hloubkou shodit PHP

Dojde ke shození PHP, nebo jen k vyjímečnému ukončení vykonávání jednoho skriptu (např. z důvodu nedostatku paměti)? Pokud to druhé, tak by to nemělo ničemu vadit, útočník si pouze odstřelí skript, který sám spouští.

ATom:

dgx: No, podle mě vyprší timeout doby běhu scriptu. Ale když někdo těch scriptů spůstí schválně hodně, tak by to mohlo začít vadit.

Lukáš:

Za ten první kód díky. Tohle jsem už dlouho potřeboval, ale nanapadlo mne, že to jde tak jednoduše :).

Havran:

A serialize($_POST) by nestacilo?

ikona Jakub Vrána OpenID:

Osobně jsem na ukládání serializovaných dat k uživateli velice opatrný, protože pak je potřeba data kontrolovat ještě pečlivěji než běžná data z formuláře - do serializovaných dat se dá uložit např. i objekt. Nicméně pokud se na to při kontrole pamatuje, tak je možné tento způsob použít.

Ondrej Ivanic:

Nie, ked uz tak skor $_REQUEST, ale
1) sierializovane data moze kto kolvek zmenit
2) problemom je rekurzia a mozeme poslat von to co nechceme.

Ked uz, potom navat vsetky polia vo forme form['meno'] a serializovat len $_REQUEST['form'] tieto data vsak treba zabezpecit aj proti zmene:

<?php
$data
= serialize($_REQUEST['form']);
// $data = base64_encode(serialize($_REQUEST['form']));
$checksum = md5($data.$tajnaKonstanta);
?>

a do formu ulozit $data a $cheksum, ktory treba znovu overit. najlepsie je ked $tajnaKonstanta bude nahodny retazec ktory ked sa vygeneruje sa ulozi do session. Lenze pri tomto pristupe je potrebne zmenit ponimanie session v php - session sa stane permanentna a timeout si musime robit sami.

Používejte diakritiku. Vstup se chápe jako čistý text, ale URL budou převedeny na odkazy a PHP kód uzavřený do <?php ?> bude zvýrazněn. Pokud máte dotaz, který nesouvisí s článkem, zkuste raději diskusi o PHP, zde se odpovědi pravděpodobně nedočkáte.

Jméno: URL: Reakce na: Ondrej Ivanic

avatar © 2005-2018 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.