PHP

Upload de Arquivos com PHP: Validação e Segurança na Prática

9 min de leitura

Upload de Arquivos com PHP: Validação e Segurança na Prática

Upload de Arquivos com PHP: Validação e Segurança Upload de arquivos é uma funcionalidade comum em aplicações web, mas também representa um dos maiores vetores de ataque se implementado incorretamente. Neste artigo, abordaremos desde o formulário HTML até técnicas avançadas de validação, focando sempre em segurança. Vou compartilhar a experiência que acumulei em anos trabalhando com essa feature crítica. Fundamentos: HTML e Configuração do PHP O Formulário HTML Correto O formulário deve usar para transmitir arquivos corretamente. Sem isso, o servidor não recebe os dados do upload. O atributo é apenas uma validação no cliente — nunca confie nela. O é processado pelo PHP, mas também adicione validação no servidor. Configurações PHP Essenciais Certifique-se de que seu está configurado corretamente: Mantenha em uma partição separada e com permissões restritas (755 ou 750). Nunca deixe como padrão do sistema. Validação Segura no Servidor Validação de Tipo de Arquivo A validação mais comum — verificar a extensão — é frágil e perigosa.

<h2>Upload de Arquivos com PHP: Validação e Segurança</h2>

<p>Upload de arquivos é uma funcionalidade comum em aplicações web, mas também representa um dos maiores vetores de ataque se implementado incorretamente. Neste artigo, abordaremos desde o formulário HTML até técnicas avançadas de validação, focando sempre em segurança. Vou compartilhar a experiência que acumulei em anos trabalhando com essa feature crítica.</p>

<h2>Fundamentos: HTML e Configuração do PHP</h2>

<h3>O Formulário HTML Correto</h3>

<p>O formulário deve usar <code>enctype=&quot;multipart/form-data&quot;</code> para transmitir arquivos corretamente. Sem isso, o servidor não recebe os dados do upload.</p>

<pre><code class="language-html">&lt;form method=&quot;POST&quot; enctype=&quot;multipart/form-data&quot;&gt;

&lt;input type=&quot;file&quot; name=&quot;arquivo&quot; accept=&quot;.pdf,.jpg,.png&quot; required&gt;

&lt;input type=&quot;hidden&quot; name=&quot;MAX_FILE_SIZE&quot; value=&quot;5242880&quot;&gt;

&lt;button type=&quot;submit&quot;&gt;Enviar&lt;/button&gt;

&lt;/form&gt;</code></pre>

<p>O atributo <code>accept</code> é apenas uma validação no cliente — nunca confie nela. O <code>MAX_FILE_SIZE</code> é processado pelo PHP, mas também adicione validação no servidor.</p>

<h3>Configurações PHP Essenciais</h3>

<p>Certifique-se de que seu <code>php.ini</code> está configurado corretamente:</p>

<pre><code class="language-ini">upload_max_filesize = 10M

post_max_size = 10M

upload_tmp_dir = /tmp/php_uploads

file_uploads = On</code></pre>

<p>Mantenha <code>upload_tmp_dir</code> em uma partição separada e com permissões restritas (755 ou 750). Nunca deixe como padrão do sistema.</p>

<h2>Validação Segura no Servidor</h2>

<h3>Validação de Tipo de Arquivo</h3>

<p>A validação mais comum — verificar a extensão — <strong>é frágil e perigosa</strong>. Um usuário mal-intencionado pode renomear um <code>.php</code> para <code>.jpg</code> e contornar essa verificação. Use a função <code>mime_content_type()</code> ou melhor ainda, a extensão <code>fileinfo</code>:</p>

<pre><code class="language-php">&lt;?php

$arquivo = $_FILES[&#039;arquivo&#039;] ?? null;

if (!$arquivo) {

die(&#039;Nenhum arquivo enviado.&#039;);

}

// Validar tamanho

$maxSize = 5 1024 1024; // 5MB

if ($arquivo[&#039;size&#039;] &gt; $maxSize) {

die(&#039;Arquivo muito grande.&#039;);

}

// Validar tipo MIME com fileinfo

$finfo = finfo_open(FILEINFO_MIME_TYPE);

$mimeType = finfo_file($finfo, $arquivo[&#039;tmp_name&#039;]);

finfo_close($finfo);

$mimePermitidos = [&#039;image/jpeg&#039;, &#039;image/png&#039;, &#039;application/pdf&#039;];

if (!in_array($mimeType, $mimePermitidos, true)) {

die(&#039;Tipo de arquivo não permitido.&#039;);

}</code></pre>

<h3>Validação de Conteúdo</h3>

<p>Para imagens, use <code>getimagesize()</code> para garantir que é realmente uma imagem válida:</p>

<pre><code class="language-php">&lt;?php

if ($mimeType === &#039;image/jpeg&#039; || $mimeType === &#039;image/png&#039;) {

$imageInfo = @getimagesize($arquivo[&#039;tmp_name&#039;]);

if ($imageInfo === false) {

die(&#039;Imagem inválida ou corrompida.&#039;);

}

if ($imageInfo[0] &gt; 4000 || $imageInfo[1] &gt; 4000) {

die(&#039;Dimensões da imagem excedem o limite.&#039;);

}

}</code></pre>

<p>Para PDFs, valide o cabeçalho do arquivo:</p>

<pre><code class="language-php">&lt;?php

if ($mimeType === &#039;application/pdf&#039;) {

$handle = fopen($arquivo[&#039;tmp_name&#039;], &#039;rb&#039;);

$header = fread($handle, 4);

fclose($handle);

if ($header !== &quot;%PDF&quot;) {

die(&#039;Arquivo PDF inválido.&#039;);

}

}</code></pre>

<h2>Armazenamento Seguro</h2>

<h3>Diretório Protegido Fora da Raiz Web</h3>

<p>Nunca armazene uploads na raiz pública (<code>/public_html</code>). Crie um diretório fora dela:</p>

<pre><code>/var/www/

├── html/ (raiz web pública)

├── uploads/ (armazenamento seguro - NÃO acessível via HTTP)

└── ...</code></pre>

<p>Configure as permissões:</p>

<pre><code class="language-bash">mkdir -p /var/www/uploads

chmod 750 /var/www/uploads

chown www-data:www-data /var/www/uploads</code></pre>

<h3>Renomeação Segura do Arquivo</h3>

<p>Nunca use o nome original do arquivo. Gere um nome único e seguro:</p>

<pre><code class="language-php">&lt;?php

$nomeOriginal = basename($arquivo[&#039;name&#039;]);

$extensao = strtolower(pathinfo($nomeOriginal, PATHINFO_EXTENSION));

// Gerar nome único

$novoNome = bin2hex(random_bytes(16)) . &#039;.&#039; . $extensao;

$caminhoDestino = &#039;/var/www/uploads/&#039; . $novoNome;

if (!move_uploaded_file($arquivo[&#039;tmp_name&#039;], $caminhoDestino)) {

die(&#039;Erro ao mover arquivo.&#039;);

}

// Armazenar no banco de dados

// INSERT INTO arquivos (nome_original, nome_armazenado, mime_type, usuario_id, data_upload)

// VALUES (?, ?, ?, ?, NOW())</code></pre>

<h3>Servir Arquivos com Segurança</h3>

<p>Crie um script download.php que valide permissões:</p>

<pre><code class="language-php">&lt;?php

session_start();

$arquivoId = $_GET[&#039;id&#039;] ?? null;

if (!$arquivoId) die(&#039;ID inválido.&#039;);

// Buscar arquivo no banco e verificar permissão do usuário

$pdo = new PDO(&#039;mysql:host=localhost;dbname=app&#039;, &#039;user&#039;, &#039;pass&#039;);

$stmt = $pdo-&gt;prepare(&#039;SELECT nome_armazenado, mime_type FROM arquivos

WHERE id = ? AND usuario_id = ?&#039;);

$stmt-&gt;execute([$arquivoId, $_SESSION[&#039;usuario_id&#039;]]);

$arquivo = $stmt-&gt;fetch();

if (!$arquivo) die(&#039;Arquivo não encontrado.&#039;);

$caminhoArquivo = &#039;/var/www/uploads/&#039; . $arquivo[&#039;nome_armazenado&#039;];

if (!file_exists($caminhoArquivo)) die(&#039;Arquivo não existe.&#039;);

header(&#039;Content-Type: &#039; . $arquivo[&#039;mime_type&#039;]);

header(&#039;Content-Disposition: attachment; filename=&quot;documento.pdf&quot;&#039;);

header(&#039;Content-Length: &#039; . filesize($caminhoArquivo));

readfile($caminhoArquivo);</code></pre>

<h2>Defesas Adicionais</h2>

<h3>Verificar Erros de Upload</h3>

<pre><code class="language-php">&lt;?php

if ($arquivo[&#039;error&#039;] !== UPLOAD_ERR_OK) {

$erros = [

UPLOAD_ERR_INI_SIZE =&gt; &#039;Arquivo excede limite ini_set&#039;,

UPLOAD_ERR_FORM_SIZE =&gt; &#039;Arquivo excede MAX_FILE_SIZE&#039;,

UPLOAD_ERR_PARTIAL =&gt; &#039;Upload interrompido&#039;,

UPLOAD_ERR_NO_FILE =&gt; &#039;Nenhum arquivo&#039;,

UPLOAD_ERR_NO_TMP_DIR =&gt; &#039;Diretório temporário ausente&#039;,

UPLOAD_ERR_CANT_WRITE =&gt; &#039;Erro ao escrever arquivo&#039;,

UPLOAD_ERR_EXTENSION =&gt; &#039;Upload bloqueado por extensão&#039;

];

die(&#039;Erro: &#039; . ($erros[$arquivo[&#039;error&#039;]] ?? &#039;Desconhecido&#039;));

}</code></pre>

<h3>Desabilitar Execução no Diretório de Uploads</h3>

<p>Se usar Apache, crie <code>.htaccess</code> no diretório de uploads:</p>

<pre><code class="language-apache">&lt;FilesMatch &quot;\.php$&quot;&gt;

Deny from all

&lt;/FilesMatch&gt;

AddType text/plain .php .phtml .php3 .php4 .php5 .php7 .phar</code></pre>

<p>Para Nginx, configure no bloco <code>location</code>:</p>

<pre><code class="language-nginx">location /uploads {

location ~ \.php$ {

return 403;

}

}</code></pre>

<h2>Conclusão</h2>

<p>Upload seguro exige <strong>defesa em profundidade</strong>. Resumo dos três pilares:</p>

<ol>

<li><strong>Validação rigorosa</strong>: Use <code>fileinfo</code> e <code>getimagesize()</code>, nunca confie apenas em extensão ou MIME-Type do cliente.</li>

</ol>

<ol>

<li><strong>Armazenamento protegido</strong>: Coloque uploads fora da raiz web, renomeie arquivos com valores aleatórios e implemente controle de acesso no código.</li>

</ol>

<ol>

<li><strong>Defesas periféricas</strong>: Configure PHP corretamente, desabilite execução de scripts no diretório de uploads e valide erros de upload.</li>

</ol>

<p>A segurança é um processo contínuo. Mantenha seu conhecimento atualizado acompanhando os CVEs relacionados a upload de arquivos.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://www.php.net/manual/en/features.file-upload.php" target="_blank" rel="noopener noreferrer">PHP: Handling File Uploads</a></li>

<li><a href="https://owasp.org/www-community/vulnerabilities/Unrestricted_File_Upload" target="_blank" rel="noopener noreferrer">OWASP: Unrestricted File Upload</a></li>

<li><a href="https://www.php.net/manual/en/book.fileinfo.php" target="_blank" rel="noopener noreferrer">PHP: fileinfo extension</a></li>

<li><a href="https://cwe.mitre.org/data/definitions/434.html" target="_blank" rel="noopener noreferrer">CWE-434: Unrestricted Upload of File with Dangerous Type</a></li>

<li><a href="https://portswigger.net/web-security/file-upload" target="_blank" rel="noopener noreferrer">PortSwigger: File Upload Vulnerabilities</a></li>

</ul>

Comentários

Mais em PHP

Boas Práticas de Superglobais em PHP: $_GET, $_POST, $_SESSION e $_COOKIE para Times Ágeis
Boas Práticas de Superglobais em PHP: $_GET, $_POST, $_SESSION e $_COOKIE para Times Ágeis

Compreendendo as Superglobais em PHP As superglobais são variáveis especiais...

Boas Práticas de Eloquent ORM: Models, Relacionamentos e Scopes para Times Ágeis
Boas Práticas de Eloquent ORM: Models, Relacionamentos e Scopes para Times Ágeis

Introdução ao Eloquent ORM O Eloquent é o Object-Relational Mapping (ORM) pad...

Segurança em PHP: XSS, CSRF, Injeção e Boas Práticas Finais: Do Básico ao Avançado
Segurança em PHP: XSS, CSRF, Injeção e Boas Práticas Finais: Do Básico ao Avançado

XSS (Cross-Site Scripting) XSS ocorre quando um atacante injeta código JavaSc...