ANÁLISE ESTÁTICA PARA A ORIENTAÇÃO E … · vii ABSTRACT In the scientific community, the use...
Transcript of ANÁLISE ESTÁTICA PARA A ORIENTAÇÃO E … · vii ABSTRACT In the scientific community, the use...
ANÁLISE ESTÁTICA PARA A ORIENTAÇÃO E
APERFEIÇOAMENTO DE CÓDIGOS EM PYTHON
Fábio Antunes Gomes
Projeto de Graduação apresentado ao Curso de
Engenharia da Computação e Informação da
Escola Politécnica, Universidade Federal do Rio
de Janeiro, como parte dos requisitos necessários à
obtenção do título de Engenheiro.
Orientador: Flávio Luis de Mello
Rio de Janeiro
Setembro de 2017
iv
UNIVERSIDADE FEDERAL DO RIO DE JANEIRO
Escola Politécnica – Departamento de Eletrônica e de Computação
Centro de Tecnologia, bloco H, sala H-217, Cidade Universitária
Rio de Janeiro – RJ CEP 21949-900
Este exemplar é de propriedade da Universidade Federal do Rio de Janeiro, que
poderá incluí-lo em base de dados, armazenar em computador, microfilmar ou adotar
qualquer forma de arquivamento.
É permitida a menção, reprodução parcial ou integral e a transmissão entre
bibliotecas deste trabalho, sem modificação de seu texto, em qualquer meio que esteja
ou venha a ser fixado, para pesquisa acadêmica, comentários e citações, desde que sem
finalidade comercial e que seja feita a referência bibliográfica completa.
Os conceitos expressos neste trabalho são de responsabilidade do(s) autor(es).
v
AGRADECIMENTO
Agradeço ao professor Flávio Luis de Mello, por toda a sua paciência e
dedicação durante a realização deste projeto.
A minha mãe e toda a minha família por todo o suporte emocional e financeiro e
minha formação.
A todos os amigos que me acompanharam nesta jornada.
E a todos os professores com quem tive a honra de estudar.
vi
RESUMO
A disponibilização de servidores de uso compartilhado para o tratamento de
dados é uma prática muito comum na comunidade científica, pois permite que seja feito
um melhor uso dos servidores através da minimização de seu tempo ocioso. Mas os
benefícios dessa prática podem ser parcialmente anulados se o código submetido ao
servidor contiver algum tipo de defeito crítico que afete a disponibilidade do servidor,
anulando os ganhos antes mencionados. Esta monografia identifica más práticas de
programação que podem levar a erros em tempo de execução ou a falhas de segurança, e
propõe uma abordagem para encontrá-las a fim de evitar a sua execução, utilizando de
análise estática de código para Python. Esta proposta dá origem a uma aplicação que
servirá como um ensaio de viabilidade. Seus objetivos são identificar padrões que
indiquem a existências das más práticas antes mencionadas e notificar o seu usuário do
local no código aonde eles foram produzidos, de maneira a orientá-lo para que aprimore
suas capacidades de desenvolvedor e seja capaz de escrever códigos de maior qualidade.
Dentre os nove fatos observados relacionados com alarmes de criticidade, um deles foi
implementado para demonstrar a viabilidade do mecanismo de análise ora apresentado.
Palavras-Chave: análise estática, más práticas, detecção de padrões, Python
vii
ABSTRACT
In the scientific community, the use of shared servers for the treatment of a large
scale of data, is a very common practice. It allows for a better use of computational
resources by minimizing the server’s idle time. But the benefits of this practice may be
reduced if the code submitted by its users contains some kind of critical defect that
compromises the server’s availability, because its execution would result in a loss of
computational resources, partially canceling the benefits before mentioned. This work
identifies bad programming practices that may result in runtime errors or security
vulnerabilities and proposes an approach to find them and avoid their execution, using
static analysis of Python code. This proposal results in an application that will serve as a
feasibility test. Its objectives are to identify patterns that suggest the use of bad
practices, described in this document, and notify the user of their localization in the
code, in order to help him or her to improve his or hers coding abilities and write better
quality code. Among the nine observed facts that represent critical indicators, one of
them was implemented to demonstrate the viability of the before mentioned analysis
mechanism.
Keywords: static code analysis, bad practices, pattern detection, Python
viii
SIGLAS
AST – Abstract Syntax Tree
DoS – Denial of Service
IDE – Integrated Development Environment
OCL – Object Constraint Language
PEP8 – Python Enhancement Proposals 8
UFRJ – Universidade Federal do Rio de Janeiro
XSS – Cross-site Scripting
ix
Sumário
LISTA DE FIGURAS ............................................................................................... XI
LISTA DE TABELAS............................................................................................. XII
CAPÍTULO 1 INTRODUÇÃO .................................................................................. 1
1.1 – TEMA ................................................................................................................ 1
1.2 – DELIMITAÇÃO ................................................................................................... 1
1.3 – JUSTIFICATIVA ................................................................................................... 1
1.4 – OBJETIVOS ........................................................................................................ 2
1.5 – METODOLOGIA .................................................................................................. 2
1.6 – DESCRIÇÃO ....................................................................................................... 3
CAPÍTULO 2 FUNDAMENTAÇÕES ....................................................................... 4
2.1 – ANÁLISE ESTÁTICA ............................................................................................ 4
2.2 – SCRIPTS BACK-END ........................................................................................... 4
2.3 – ANALISADORES SINTÁTICOS .............................................................................. 6
2.4 – LINGUAGEM PYTHON ....................................................................................... 10
2.5 – PARSERS UTILITÁRIOS PARA PYTHON ............................................................... 11
2.5.1 - Pycodestyle .............................................................................................. 11
2.5.2 - Módulo Tokenize ...................................................................................... 13
2.5.3 - Módulo Parser ......................................................................................... 15
2.5.4 – Módulo AST (Abstract Syntax Tree) ......................................................... 17
2.6 – TRABALHOS RELACIONADOS ........................................................................... 18
CAPÍTULO 3 SOLUÇÃO PROPOSTA .................................................................. 20
3.1 – DESCRIÇÃO DO PROBLEMA .............................................................................. 20
3.2 – DESCRIÇÃO DA SOLUÇÃO ................................................................................. 20
3.3 – FATOS OBSERVADOS ....................................................................................... 23
3.3.1 – Loops While ............................................................................................. 23
3.3.2 – Loops For ................................................................................................ 23
3.3.3 – Geração de Threads ................................................................................ 24
3.3.4 – Acesso a Recursos Externos ..................................................................... 26
x
3.3.5 – Saltos incondicionais ............................................................................... 27
3.3.6 – Comunicação entre processos .................................................................. 27
3.3.7 – Acesso a Portas ....................................................................................... 29
3.3.8 – Acesso a Recursos Lentos ........................................................................ 29
3.3.9 – Reflexão ................................................................................................... 30
3.4 – O LOOP FOR .................................................................................................... 31
3.4.1 – Conjuntos de objetos ................................................................................ 33
3.5 – DESAFIOS DA ANÁLISE ESTÁTICA ..................................................................... 34
CAPÍTULO 4 IMPLEMENTAÇÃO ........................................................................ 37
4.1 – DETERMINANDO O TIPO DE UMA VARIÁVEL ..................................................... 38
4.2 – OS TIPOS ......................................................................................................... 38
4.3 – VARIÁVEIS E CONTAINERS ............................................................................... 39
4.4 – GESTORES DE CONTEXTO ................................................................................. 40
4.4.1 – Atribuição (Assign) .................................................................................. 41
4.4.2 – Controle de Fluxo .................................................................................... 41
4.4.3 – Definição de Função ................................................................................ 42
4.4.4 – Chamada de Função ................................................................................ 42
4.4.5 - Definição de Classe .................................................................................. 42
4.5 – REFERÊNCIAS .................................................................................................. 44
4.6 – IMPLEMENTANDO A REGRA DO LOOP FOR ........................................................ 44
4.7 –TESTES DE SANIDADE ....................................................................................... 47
CAPÍTULO 5 CONCLUSÃO ................................................................................... 49
5.1 – CONCLUSÕES ................................................................................................... 49
5.2 – TRABALHOS FUTUROS ..................................................................................... 49
BIBLIOGRAFIA ...................................................................................................... 51
ANEXO A BASE DE TESTES ................................................................................. 54
xi
Lista de Figuras
2.1 – Diagrama de porcentagem de uso de linguagens em back-end . . . . . . . . . . . 11
3.1 – Diagrama de funcionamento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
4.1 – Diagrama de Classes Simplificado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
4.2 – Diagrama de Classes Detalhado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
xii
Lista de Tabelas
2.1 – Exemplo de sequência de análise top-down . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.2 – Exemplo de sequência de análise bottom-up . . . . . . . . . . . . . . . . . . . . . . . . . 9
1
Capítulo 1
Introdução
1.1 – Tema
O tema deste trabalho é a identificação de um conjunto de práticas de
programação em Python que frequentemente resultam em erros em tempo de execução
ou potenciais vulnerabilidades. O problema a ser resolvido é propor uma abordagem
para localização de más práticas de programação utilizando análise estática de código,
de modo a alertar um programador de possíveis melhorias a serem feitas no programa
analisado, para gerar um código de maior qualidade.
1.2 – Delimitação
Este trabalho foi feito tendo em mente o contexto de servidores cujo uso é
compartilhado por vários colaboradores. Nele pressupõe-se que os usuários não têm fim
malicioso, uma vez que já estariam autorizados a utilizar o servidor por meio de algum
sistema de autenticação.
Este trabalho será uma prova de conceito que poderá servir como base para a
criação de uma aplicação capaz de encontrar estaticamente más práticas em programas
Python. Para tal será apresentada a implementação de uma aplicação capaz de encontrar
as más práticas relativas a loops for, identificadas neste documento.
1.3 – Justificativa
A disponibilização de servidores para o tratamento de dados é uma prática muito
comum na comunidade científica. Isso permite que diferentes pessoas dentro de uma
mesma organização tenham acesso simultâneo a máquinas capazes de processar grandes
2
volumes de dados, sem que seja necessário adquirir uma máquina individual para cada
uma dessas pessoas. Adicionalmente, essa prática faz com que haja uma melhor
utilização dos recursos disponíveis, já que com múltiplos usuários, as máquinas terão
menos tempo ocioso. Porém, o fato desta prática criar um regime de concorrência, traz o
potencial risco de indisponibilidade por falha do servidor, causado pela submissão de
tarefas contendo erros.
Neste contexto, uma aplicação capaz de detectar tais erros de maneira
preventiva, economizaria uma expressiva quantidade de recursos. Ela traria ainda o
benefício adicional de agilizar o processo de depuração de erros, uma vez que, sendo
capaz de detectá-los, a aplicação indicaria para o programador tal problema. Além
disso, como efeito colateral, há a possibilidade de reduzir a indisponibilidade dos
servidores, um impedimento relacionado com o mau uso de seus recursos.
Atualmente o Pylint é o analisador de código mais utilizado para Python. Seus
principais objetivos são verificar se o código por ele analisado segue o guia de estilo de
Python (PEP8) e determinar se o programa apresenta erros simples, como tentar utilizar
uma variável antes dela ser inicializada. A aplicação proposta neste trabalho também
terá como objetivo ajudar no aprimoramento do código analisado, porém isso será feito
com uma abordagem diferente do Pylint, tentando tratar questões de maior
complexidade.
1.4 – Objetivo
Este trabalho tem como objetivo a elaboração de uma prova de conceito que
poderá servir como base para a criação de uma aplicação capaz de encontrar
estaticamente más práticas em programas Python.
1.5 – Metodologia
Para a realização deste trabalho, inicialmente foi definido um conjunto de
práticas na programação Python que frequentemente levam a erros em tempo de
3
execução ou a potenciais falhas de segurança. Boa parte do conhecimento necessário
para determinar quais são as possíveis más práticas foi adquirido durante meu duplo
diploma na França, onde tive a oportunidade de trabalhar em uma empresa da área de
segurança que fazia testes de intrusão em aplicações web.
Em seguida foi criada uma aplicação capaz de prever a ocorrência de um desses
tipos de evento em programas Python, antes do início de sua execução em um servidor.
Sua finalidade é evitar o incremento da probabilidade de falha do servidor, através da
prevenção de execução de códigos defeituosos. No final, para testar o funcionamento da
aplicação, foram criados programas contendo as vulnerabilidades previstas e verificado
se a aplicação era capaz de encontrá-las.
1.6 – Descrição
No Capítulo 2 é apresentado o contexto para o qual a aplicação está sendo
projetada. Ele começa com uma breve descrição de análise estática, seguida de uma
explicação resumida de analisadores sintáticos. Depois é apresentada a linguagem
Python e quais tecnologias foram utilizadas para a concepção da aplicação, e no final é
feita uma menção a trabalhos relacionados.
A solução proposta é abordada no Capítulo 3, iniciando-se com a descrição do
problema, seguida da apresentação da proposta de solução em si. Após são descritas as
más práticas identificadas no trabalho e explicadas as escolhas de regras de inferência.
Este capítulo termina com a descrição do funcionamento do loop for e dos desafios
apresentados pela análise estática.
No Capítulo 4 é detalhada a implementação da aplicação e é apresentada a
maneira como foram conduzidos os seus testes de sanidade.
O Capítulo 5 finaliza o documento, avaliando se os objetivos descritos na Seção
1.4 foram obtidos e propõe aprimoramentos para a aplicação.
4
Capítulo 2
Fundamentações
2.1 – Análise Estática
A análise estática consiste em analisar um código fonte, em uma tentativa de
encontrar erros ou vulnerabilidades, sem que o código seja executado [13]. No ciclo de
produção de um software, este tipo de análise geralmente é realizado antes da análise
dinâmica, como testes unitários e testes de integração.
Entre suas vantagens em relação ao testes dinâmicos, pode-se citar a
escalabilidade [13], o fator dela permitir que erros sejam encontrados mais cedo no ciclo
de desenvolvimento de um software e a sua capacidade de indicar a localização exata do
código aonde o erro foi produzido [5]. Em contrapartida a análise estática tem a
desvantagem de produzir falsos positivos e falsos negativos, e ser incapaz de analisar
eventos produzidos em tempo de execução, como dados de entrada do usuário.
2.2 – Scripts Back-end
Scripts são programas escritos para ambientes computacionais, contendo
sequências de comandos que automatizam a execução de tarefas que poderiam ser
realizadas individualmente por um operador. Um exemplo muito conhecido de
linguagem de scripting é a linguagem bash, utilizada nos terminais Linux.
O termo back-end refere-se a tudo que não é interface com o usuário. No
contexto do modelo cliente-servidor, seria tudo que se encontra no lado do servidor,
como por exemplo, bancos de dados e o sistema que comanda a lógica de um website.
A utilização de scripts back-end em um ambiente web, refere-se à utilização de
software no lado do servidor para executar algum tipo de código, ao invés de utilizar
5
recursos do lado cliente, como por exemplo, um navegador. No contexto web isso pode
ser utilizado para gerar páginas dinamicamente, sem criar uma carga extra para o
navegador do cliente e sem necessitar que o mesmo possua algum software específico
instalado.
Essa técnica pode ainda permitir que um cliente envie instruções para serem
executadas no servidor. Com isso usuários podem ter acesso remoto a máquinas com
capacidade de processamento e acesso a dados diferenciados daqueles disponíveis
localmente. Contudo, esse tipo de uso traz também consigo uma série de desafios.
Um deles é lidar com a concorrência entre múltiplos usuários tentando acessar
os mesmos recursos do servidor simultaneamente. Isso normalmente é gerido pelo
próprio sistema operacional do servidor mas pode ainda assim ser um problema, pois se
não houver um mecanismo eficiente que regule o quanto um usuário pode demandar por
vez, pode ocorrer uma sobrecarga do servidor por um ou mais jobs, o que
consequentemente levaria a uma negação de serviço.
Outro desafio é se certificar que os recursos do servidor não estão sendo
desperdiçados, intencionalmente ou não, pelos usuários. Erros simples em um script,
como a falta de uma condição de parada de um loop, pode fazer com que um programa
não termine de rodar antes que seu tempo limite de execução seja esgotado. Esse tipo de
erro, além de consumir uma elevada quantidade de tempo de processamento, fará com
que um script não gere resultados conclusivos, o que configura um completo
desperdício dos recursos de servidor.
Um terceiro desafio é garantir que o código enviado por um cliente é seguro.
Pela própria natureza deste tipo de aplicação que roda em um servidor, há uma
probabilidade elevada de que ela seja usada para fins maliciosos. Um hacker poderia
por exemplo, ganhar acesso aos servidores através da exploração de alguma
vulnerabilidade, ou usar o servidor de uma maneira não prevista para atacar outras
máquinas na rede.
2.3 – Analisadores Sintáticos
6
A análise sintática é o processo de analisar uma sequência de símbolos,
conforme as regras de uma gramática formal. No contexto de linguagens de
programação, um analisador sintático é um componente de software responsável por
verificar que um código de entrada, previamente dividido em tokens por um analisador
léxico, esteja de acordo com as regras estabelecidas pela sua gramática. Ele é
responsável também por construir uma estrutura de dados, como por exemplo uma
árvore de análise, que representa os dados de entrada de forma estruturada.
Geralmente os analisadores sintáticos são utilizados como uma etapa
intermediária no processo de compilação, cujos dados de saída serão posteriormente
usados por um analisador semântico. Alguns outros usos são o emprego em IDEs, para
identificar erros de sintaxe durante o processo de desenvolvimento, e como componente
de verificação ortográfica para corretores de texto automatizados.
Existem dois tipos de abordagens para um analisador sintático, top-down e
bottom-up. A primeira abordagem começa a partir da raiz da árvore de derivação e vai
expandindo os nós da árvore até encontrar um token (folha) que corresponda a um
símbolo da sequência que está sendo analisada. A segunda tem um comportamento
oposto à primeira, começando a partir das folhas, ele converte a sequência de entrada
em tokens e faz o caminho inverso, reduzindo combinações de tokens e termos em
outros termos até chegar ao nó raiz.
Para melhor compreender estas abordagens, será usada a seguinte gramática
como exemplo:
1 <goal> ::= <expr>
2 <expr> ::= <term> <expr'>
3 <expr'> ::= + <expr>
4 | - <expr>
5 | Є
6 <term> ::= <factor> <term'>
7 <term'> ::= * <term>
8 | / <term>
7
9 | Є
10 <factor> ::= number
11 | id
Ambos os exemplos terão a seguinte string de entrada: x - 2 * y
Tanto a gramática deste exemplo, quanto a sua resolução no caso top-down
foram retiradas das aulas do curso Compiler Design [23], conduzidas pelo professor
Lawrence Rauchweger na Texas A&M University. A resolução do caso bottom-up foi
feita pelo autor baseada nas aulas do mesmo curso.
2.3.1 – Análise top-down
A tabela a seguir possui quatro colunas. A primeira coluna denota o passo atual
do algoritmo, a segunda a expressão utilizada na iteração, a terceira a forma sequencial
das folhas da árvore de derivação encontrada ao final do passo em questão e a quarta
mostra qual símbolo de entrada está sendo analisado no momento (precedido pelo
caractere ↑).
Tabela 2.1- Exemplo de sequência de análise top-down
Passo Expr Forma Sequencial Símbolo de Entrada
0 ― <goal> ↑x - 2 * y
1 1 <expr> ↑x - 2 * y
2 2 <term> <expr'> ↑x - 2 * y
3 6 <factor> <term'> <expr'> ↑x - 2 * y
4 11 id <term'> <expr'> ↑x - 2 * y
5 ― id <term'> <expr'> x ↑- 2 * y
6 9 id <expr'> x ↑- 2 * y
7 4 id - <expr> x ↑- 2 * y
8 ― id - <expr> x - ↑2 * y
9 2 id - <term> <expr'> x - ↑2 * y
8
10 6 id - <factor> <term'> <expr'> x - ↑2 * y
11 10 id - number <term'> <expr'> x - ↑2 * y
12 ― id - number <term'> <expr'> x - 2 ↑* y
13 7 id - number * <term> <expr'> x - 2 ↑* y
14 ― id - number * <term> <expr'> x - 2 * ↑y
15 6 id - number * <factor> <term'> <expr'> x - 2 * ↑y
16 11 id - number * id <term'> <expr'> x - 2 * ↑y
17 ― id - number * id <term'> <expr'> x - 2 * y↑
18 9 id - number * id <expr'> x - 2 * y↑
19 5 id - number * id x - 2 * y↑
O analisador começa com o nó raiz em 0, e o expande usando a expressão 1 no
passo 1. Ele continua esse padrão de expandir o primeiro termo até que ele encontre um
token que seja compatível ao primeiro símbolo do input (passo 4), no caso o x que é um
id.
Em seguida ele tenta o mesmo para o segundo símbolo ( - ), mas como não é
possível chegar neste token através da expansão do termo <term'>, ele o considera como
vazio (Є) usando a expressão 9, e continua expandindo o termo seguinte até encontrar o
símbolo que procura (passo 7).
Esse comportamento se repete para todos os símbolos do input, até que no passo
17 chega-se ao fim dos símbolos. Como ainda existem termos que não foram totalmente
expandidos, o algoritmo tenta anulá-los, transformando-os em vazio. No final (passo
19) tem-se a expressão exata que corresponde à entrada.
2.3.2 – Análise bottom-up
A tabela a seguir possui as mesmas quatro colunas do exemplo top-down, com a
diferença que desta vez o símbolo “↑” precede o próximo caractere a ser lido.
9
Tabela 2.2- Exemplo de sequência de análise bottom-up
Passo Expr Forma Sequencial Símbolo de Entrada
0 ― ↑x - 2 * y
1 Shift Id x ↑- 2 * y
2 11 <factor> x ↑- 2 * y
3 9 <factor> <term'> x ↑- 2 * y
4 6 <term> x - ↑2 * y
5 Shift <term> - x - 2 ↑* y
6 Shift <term> - number x - 2 ↑* y
7 10 <term> - <factor> x - 2 ↑* y
8 Shift <term> - <factor> * x - 2 * ↑y
9 Shift <term> - <factor> * id x - 2 * y↑
10 11 <term> - <factor> * <factor> x - 2 * y↑
11 7 <term> - <factor> * <factor> <term'> x - 2 * y↑
12 6 <term> - <factor> * <term> x - 2 * y↑
13 7 <term> - <factor> <term'> x - 2 * y↑
14 6 <term> - <term> x - 2 * y↑
15 5 <term> - <term> <expr'> x - 2 * y↑
16 2 <term> - <expr> x - 2 * y↑
17 4 <term> <expr'> x - 2 * y↑
18 2 <expr> x - 2 * y↑
19 1 <goal> x - 2 * y↑
O analisador começa com o primeiro símbolo (x), o identificando como id. Em
seguida ele determina que a única expressão que começa com um id é a 11,
transformando este id em <factor> (passo 2). Ele repete o passo anterior novamente,
10
determinando que a única expressão que teria como primeiro termo um <factor> seria a
6, logo o termo que segue a atual forma sequencial deve ser um <term’>, como o
próximo símbolo de entrada (“-”) não corresponde a nenhuma das expressões de
<term’> ele assume que existe um símbolo vazio e usa a expressão 9 seguida da
expressão 6 (passos 3 e 4).
Repetindo o processo do passo 2, o analisador verifica que a única expressão
que possui um <term> como seu primeiro elemento é a expressão 2, logo a atual forma
sequencial deve ser sucedida de um termo do tipo <expr’>. Mas desta vez existe uma
expressão para <expr’> iniciada por um símbolo “-”, então o analisador adiciona este
símbolo a sua forma sequencial (passo 5) e determina que ele será sucedido por <expr>.
E desta forma, o analisador continua combinando as expressões da gramática, para que
elas correspondam aos símbolos de entrada, até o momento que ele finalmente consegue
chegar na forma <term><expr’> (passo 17), a qual ele visava desde o passo 5. E depois
ele continua com o mesmo comportamento até chegar na raiz da árvore.
2.4 – Linguagem Python
Python é uma linguagem de criada em 1991 por Guido van Rossum. Seu design
foi voltada para uma filosofia que enfatiza a legibilidade [1], reforçando isso através
obrigatoriedade de indentação para delimitar blocos de código e pela sintaxe simples
que torna a linguagem parecida com pseudo-código executável [2]. Sua tipagem é
dinâmica, fazendo com que objetos não possuam um tipo rígido e evita a necessidade de
inicializa-los previamente a sua utilização.
Python é uma linguagem interpretada, isso significa que, ao contrário de
linguagens compiladas, um programa em Python não precisa passar por uma etapa de
compilação, na qual o código fonte é convertido para linguagem de máquina. Ao invés
disso, ele é lido e executado diretamente por um programa chamado de interpretador. A
ausência da necessidade de compilação faz com que o ciclo de desenvolvimento, teste e
implantação de um programa seja mais rápido, tornando a linguagem bastante atrativa
no meio científico e para prototipagem. Porém, esta característica traz consigo a
11
desvantagem de ter um maior tempo de execução esperado, em relação às linguagens
compiladas [3].
Apesar das vantagens mencionadas, Python não é uma linguagem muito
utilizada em servidores, entre outros motivos, devido ao seu maior tempo de execução
esperado, que implica em maiores custos. Em seguida, encontra-se um diagrama com o
uso de linguagens de programação no back-end de websites.
Figura 2.1- Diagrama de porcentagem de uso de linguagens em back-end (adaptado)
Fonte: w3techs [4]
2.5 – Parsers Utilitários para Python
2.5.1 - Pycodestyle
Pacote de Python utilizado para verificar se um código fonte de entrada está
seguindo as convenções do guia de estilo do Python, conhecido como PEP8 [20]. Em
um primeiro momento um analisador de estilo precisa também fazer o parsing do
código. Logo, analisar o método utilizado pelo pycodestyle para fazer esse parsing foi
uma fonte importante de informação, que posteriormente levou à descoberta do módulo
mais adequado chamado tokenize.
12
Exemplo:
pycodestyle_test_script.py
1 #Lack of space after the comment symbol
2 import math
3
4 def test_function():
5 print "Should have missing white space error after this line"
6
7 for i in range(10):
8 print i
Para o código acima, o pycodestyle encontrou os seguintes erros:
pycodestyle_test_script.py:1:1: E265 block comment should start with '# '
pycodestyle_test_script.py:4:1: E302 expected 2 blank lines, found 1
pycodestyle_test_script.py:7:1: E305 expected 2 blank lines after class or
function definition, found 1
O primeiro aponta a falta de um espaço após o símbolo de comentário. Essa
verificação pode ser feita facilmente verificando os dois primeiros caracteres das tokens
de COMMENT. No caso o python reconhece a seguinte token para a primeira linha:
COMMENT '#Lack of space after the comment symbol'. Como o segundo caracter não
é um espaço em branco, o pycodestyle reconhece um erro de estilo na primeira linha.
O segundo aponta a falta de duas quebras de linha após o bloco de início do
programa, geralmente contendo comentários e importações de outros módulos. Para isso
o pycodestyle utiliza uma expressão regular que verifica o fim deste bloco e conta o
número de tokens NL que o seguem para verificar se existem exatamente 2.
O terceiro aponta a falta de duas quebras de linha após a definição da função
test_function(). Sua verificação é feita de maneira similar à do erro anterior. Após
encontrar o fim de um bloco de declaração de função ou classe, ele verifica que os dois
últimos tokens são do tipo NL e envia um erro caso não sejam.
13
2.5.2 - Módulo Tokenize
Módulo da biblioteca padrão do Python que fornece um scanner léxico para
código fonte Python. Sua principal função, que possui o mesmo nome do módulo,
consegue ler um código fonte de Python e o decompor nas tokens básicas
compreendidas pelo interpretador. [21]
Do output da função tokenize, podem ser extraídas informações essenciais para a
realização deste projeto, como por exemplo o início e fim de um bloco (if, while, for e
etc), através das tokens INDENT e DEDENT e o fim de cada uma das linhas de código
através do token NEWLINE. Essa token é ainda diferenciada de um statement que
utiliza várias linhas (como a inicialização de tuplas e listas), pois caso isso aconteça ele
usa a token NL, ao invés da token NEWLINE.
Exemplo:
#Código
i = 0
while i < 5:
i += 1
print(i)
print("Hello")
#Output do Tokenize
0,0-0,0: ENCODING 'utf-8'
1,0-1,1: NAME 'i'
1,2-1,3: OP '='
1,4-1,5: NUMBER '0'
1,5-1,6: NEWLINE '\n'
2,0-2,5: NAME 'while'
2,6-2,7: NAME 'i'
2,8-2,9: OP '<'
2,10-2,11: NUMBER '5'
14
2,11-2,12: OP ':'
2,12-2,13: NEWLINE '\n'
3,0-3,4: INDENT ' '
3,4-3,5: NAME 'i'
3,6-3,8: OP '+='
3,9-3,10: NUMBER '1'
3,10-3,11: NEWLINE '\n'
4,4-4,9: NAME 'print'
4,9-4,10: OP '('
4,10-4,11: NAME 'i'
4,11-4,12: OP ')'
4,12-4,13: NEWLINE '\n'
5,0-5,0: DEDENT ''
5,0-5,5: NAME 'print'
5,5-5,6: OP '('
5,6-5,13: STRING '"Hello"'
5,13-5,14: OP ')'
5,14-5,15: NEWLINE '\n'
6,0-6,0: ENDMARKER ''
Neste exemplo temos todos os tokens encontrados pelo módulo tokenize. Os dados de
saída estão divididos em três colunas, onde a coluna da esquerda representa a posição de
início (linha e coluna) do token e sua posição de fim, separados por hífen. A coluna
central representa a classificação daquele token e a coluna da direita mostra o token em
si.
Como pode ser visto no exemplo, o primeiro token representa sempre o
encoding que está sendo usado no código fonte fornecido. Nas quatro tokens seguintes
encontra-se a decomposição da primeira linha do código (i = 0), que é uma simples
atribuição de valor, contendo uma token do tipo NAME, que representa a variável i,
seguida por uma token do tipo OP, para o operador “=”, uma token NUMBER para o
15
número 0 e finalmente uma token NEWLINE para representar o fim da linha de código.
Para a segunda linha temos a linha onde se inicia o loop while, que é
representada por uma token NAME(while), seguida da condição do loop representada
por 3 tokens: NAME(i), OP(<) e NUMBER(5). Essa condição é seguida de uma token
OP para o operador “:” e uma token de NEWLINE para fazer a quebra de linha.
Após a linha que define a condição de parada do loop, temos o bloco de código
iniciado na linha três pela token INDENT e se estendendo até o final da linha quatro, e
finalizado no início da linha cinco pela token DEDENT. Este bloco segue os mesmo
padrões anteriormente utilizados. Os nomes de variáveis e funções são descritos por
tokens do tipo NAME, todos os tipos de operadores, incluindo parênteses, são descritos
por tokens do tipo OP, números são descritos por tokens NUMBER e ao final de cada
linha existe uma token NEWLINE.
Na linha cinco encontra-se um exemplo de uma chamada da função print,
passando como parâmetro uma string. Nela encontra-se uma token NAME para o nome
da função, uma token STRING que representa a string “Hello” rodeada pelos dois
tokens de operadores parênteses (OP) e terminada com uma quebra de linha
(NEWLINE). E para marcar o fim do programa, é utilizada uma token
ENDMARKER.
Apesar da delimitação precisa que este módulo dá de cada bloco de código, os
dados de retorno deste módulos são muito simples. Sua utilização como ponto inicial da
aplicação demandaria uma grande quantidade de trabalho para transformar as sequência
de tokens, por ele fornecida, em estruturas de dados que facilitassem a implementação
das regras de inferência que serão explicadas posteriormente.
2.5.3 - Módulo Parser
Módulo da biblioteca padrão Python que fornece uma interface ao analisador
sintático interno do Python e seu compilador de byte-code. Seu principal propósito é
permitir que um código Python seja capaz de modificar a árvore sintática de uma
expressão e gerar código executável a partir dela. [19]
Para um código fonte extremamente simples, por exemplo um programa
16
composto apenas da linha print("Hello World"), pode-se gerar uma árvore sintática
(st), através da função parser.suite(). Essa árvore por sua vez pode ser convertida em
um formato de listas ou tuplas, através das funções parser.st2list() e parser.st2tuple(),
gerando a seguinte lista:
[257, [267, [268, [269, [272, [1, 'print', 1], [304, [305, [306, [307, [308, [310, [311,
[312, [313, [314, [315, [316, [317, [318, [7, '(', 1], [320, [304, [305, [306, [307, [308,
[310, [311, [312, [313, [314, [315, [316, [317, [318, [3, '"Hello World"',
1]]]]]]]]]]]]]]]], [8, ')', 1]]]]]]]]]]]]]]]]], [4, '', 1]]], [4, '', 1], [0, '', 1]]
Ou com os números inteiros traduzidos para os símbolos e tokens que eles representam
tem-se a seguinte árvore:
['file_input', ['stmt', ['simple_stmt', ['small_stmt', ['print_stmt', ['NAME', 'print',
'NAME'], ['test', ['or_test', ['and_test', ['not_test', ['comparison', ['expr',
['xor_expr', ['and_expr', ['shift_expr', ['arith_expr', ['term', ['factor', ['power',
['atom', ['LPAR', '(', 'NAME'], ['testlist_comp', ['test', ['or_test', ['and_test',
['not_test', ['comparison', ['expr', ['xor_expr', ['and_expr', ['shift_expr',
['arith_expr', ['term', ['factor', ['power', ['atom', ['STRING', '"Hello World"',
'NAME']]]]]]]]]]]]]]]], ['RPAR', ')', 'NAME']]]]]]]]]]]]]]]]], ['NEWLINE', '',
'NAME']]], ['NEWLINE', '', 'NAME'], ['ENDMARKER', '', 'NAME']]
Se necessário seria possível modificar a sub-lista que contém a string "Hello
World", transformá-la novamente em um objeto st, com a função parser.sequence2st(),
e a compilar novamente em um objeto de código que pode ser avaliado pela função
eval().
Esse módulo é interessante para esse projeto, por causa de sua capacidade de
fornecer a árvore sintática completa de uma string de código Python, através das
funções st2tuple e st2list. Mas ao mesmo tempo, o excesso de detalhe das árvores
geradas e a dificuldade de acessar um elemento em específico podem vir a tornar o
processo de analisar um código fonte muito complexo e difícil para o escopo deste
17
trabalho.
2.5.4 – Módulo AST (Abstract Syntax Tree)
Pacote de Python que semelhantemente ao módulo Parser gera a árvore
sintática de um programa em Python. A sua principal diferença é que a árvore gerada
por este módulo está encapsulada em estruturas de dados que tornam mais simples a sua
leitura e interpretação [22], o que foi o principal motivo para a escolha deste módulo
para servir de base para a aplicação que será apresentada no curso deste trabalho. Em
seguida está representada a estrutura gerada por AST para um for simples.
# Código:
for i in a:
print i
# Estrutura gerada pelo módulo ast:
For(target = Name(id='i', ctx=Store()),
iter = Name(id='a', ctx=Load()),
body = [Print(dest=None, values=[Name(id='i', ctx=Load())], nl=True)],
orelse = []
)
No exemplo acima pode-se observar que o for foi encapsulado em um objeto
For que possui quatro atributos. O primeiro, target, referencia as variáveis às quais o
loop atribuirá um valor, no caso a variável i. O segundo, iter, representa o item sobre o
qual será iterado, no exemplo a variável a. O terceiro, o atributo body, referencia uma
lista das instruções que serão executadas durante o loop, neste caso contendo apenas
uma instrução do tipo Print. E o último atributo (orelse) consiste de uma lista de todas
as instruções que seriam executadas ao final do loop, no caso nenhuma pois este for não
possui um Else no final.
2.6 – Trabalhos Relacionados
18
Seifert e Samlaus [6] apresentam um método para fazer a análise estática de um
código fonte de uma linguagem genérica. Ele utiliza como entrada a definição de
sintaxe da linguagem na qual o código fonte a ser analisado foi escrito, e desta forma
gerar sua árvore sintática. Em seguida ele procura na árvore sintática padrões
indesejáveis definidos em uma linguagem OCL (Object Constraint Language), como
por exemplo a declaração de uma variável cujo nome tem tamanho inferior a um valor
especificado. O interesse deste artigo é que ele mostra um método de procurar padrões
em código fonte estaticamente, através da árvore sintática abstrata.
Kuo et al. [7] propõem uma abordagem para tentar identificar estaticamente
falhas de segurança em códigos fontes. A linguagem nele estudada é PHP, que possui
algumas semelhanças com Python, no aspecto de ser uma linguagem de script,
interpretada e com tipagem dinâmica. No artigo são propostos alguns conceitos como a
análise do fluxo no código, para determinar quais variáveis podem estar “infectadas”,
em outras palavras, variáveis cujo conteúdo foi submetido pelos usuários em tempo de
execução e portanto podendo conter código malicioso. A partir dessa classificação é
definido se essas variáveis podem ou não ser usadas em funções mais críticas, como por
exemplo a função eval, que faz o uso de reflexão (interpretação de código gerado em
tempo de execução). As ideias nele propostas, apesar de terem um viés para a área de
segurança, podem ser adaptadas para a busca de más práticas de programação, ainda
que não tenham um intuito malicioso.
Gomes [8], em sua dissertação, apresenta a Plataforma Tile-in-One e propõe o
uso de algoritmos de aprendizado de máquina para a identificação preventiva de falhas
em códigos fonte. Este trabalho é importante no sentido de que ele é o precursor do que
será apresentado nos capítulos a seguir, que tentam dar uma nova visão ao que já foi
criado previamente para a plataforma Tile-in-One, apesar de utilizar uma abordagem
distinta.
Dahse [17] introduz uma ferramenta cujo objetivo é reduzir o tempo necessário
para fazer um teste de intrusão de uma aplicação PHP. Essa ferramenta faz uma análise
estática do código fonte da aplicação, utilizando o conceito de análise de variáveis
“infectadas” (“taint analysis”) anteriormente mecionada. Ela utiliza principalmente os
19
tokens gerados pela função token_get_all para encontrar as funções e variáveis
“infectadas”. Apesar da análise token a token não ser a mais indicada para o que será
feito neste trabalho, Dahse utiliza-se de um pré-processamento, removendo alguns tipos
de tokens e substituindo alguns caracteres especiais. Esta ideia pode vir a ser útil para
facilitar o tratamento do código neste projeto.
Jovanovic et al.[18] implementam um protótipo de aplicação focada na detecção
de cross-site scripting (XSS) em códigos PHP, batizada de Pixy. Ela utiliza
principalmente conceitos de “taint analysis” e análise do fluxo de dados, já
mencionados anteriormente. No artigo é mencionado também o conceito de “alias
analysis”, que consiste em verificar todas as variáveis que apontam para um
determinado objeto. Este conceito será importante para a verificação da regra do loop
for, que será apresentada posteriormente neste trabalho.
20
Capítulo 3
Solução Proposta
3.1 – Descrição do Problema
Em um ambiente de um servidor compartilhado, no qual múltiplos
colaboradores podem submeter programas para execução, garantir que os códigos a
serem executados não possuam erros é especialmente importante, pois se trata de um
regime de concorrência. Porém, muitas vezes os usuários destes ambientes não tem
como principal preocupação a qualidade do código que estão produzindo, uma vez que
os problemas que eles têm a resolver são, naturalmente, de maior importância.
Contudo, esta falta de preocupação com a qualidade do código, muitas vezes
levam a más práticas de programação, que por sua vez podem resultar na submissão de
programas contendo erros ou vulnerabilidades. Esta situação se torna ainda pior quando
é feita a reutilização de código por parte dos colaboradores, que pode resultar na
propagação dos erros antes mencionados. Desta forma, deseja-se fazer a detecção destas
más práticas, antes de sua execução, evitando as possíveis consequências de sua
submissão e orientando os usuários para que possam aprimorar suas habilidades ao
programar.
3.2 – Descrição da Solução
Para resolver este problema, foram selecionados uma série de erros e más
práticas, que acontecem com frequência durante o processo de desenvolvimento de um
programa, especificamente para Python. Os erros e más práticas identificados neste
trabalho são:
O uso de loops while para iterar sobre um conjunto fixo de elementos.
Alterar a quantidade de elementos de um conjunto sobre o qual se está iterando
21
com um loop for.
Uso indevido de threads.
Acesso a recursos externos não confiáveis.
Mal uso de saltos incondicionais.
Comunicação perniciosa entre processos
Uso de funções que façam ou liberem o acesso a portas.
Acesso desnecessário a recursos lentos.
Uso indevido de reflexão.
Uma vez listados estes erros, foi construída uma aplicação em Python capaz de
ler o código fonte bruto e identificar as diferentes estruturas do código. A partir destas
estruturas ela é capaz de encontrar padrões que indicam a existência desses erros e más
práticas que, a partir deste momento serão chamados de fatos observados, são descritos
na seção seguinte.
O funcionamento da aplicação encontra-se ilustrado na Figura 3.1. Ela começa
com a submissão de um programa em Python, por parte do usuário. O código é lido e
transformado em uma árvore sintática, pelo módulo AST da biblioteca padrão de
Python. Em seguida esta árvore é enviada para o analisador de código, que vai procurar
padrões que indiquem a ocorrência de cada um dos fatos antes mencionados.
Finalmente, com base na quantidade de ocorrências de cada fato, é calculada a
periculosidade do programa, que determina se ele poderá ou não ser executado no
servidor.
23
3.3 – Fatos Observados
3.3.1 – Loops While
Loops do tipo while geralmente são evitados no contexto de Python. O seu
principal motivo é que na maioria das vezes em que se deseja utilizar um loop, o
objetivo é iterar sobre um conjunto fixo de elementos, tipicamente uma lista cujo
tamanho não será alterado durante o loop. Neste caso, deve-se dar preferência ao loop
for, porque a própria maneira na qual ele é construído evita a ocorrência de loops
infinitos resultantes de erros. Além disso, quando comparados um loop for e um loop
while com um contador de loop explícito, executando a mesma função sobre um mesmo
conjunto iterável, o while sempre será mais lento [9].
Devido à grande versatilidade do loop for em Python, o loop while é geralmente
limitado a casos em que não se conhece o número de iterações a serem executadas,
como por exemplo quando se precisa de algum tipo de dado de entrada do usuário [15].
Por isso o mais comum é propositalmente definir loops while como loops infinitos
(while True), aliado ao uso da instrução break como condição de parada.
Proposta de índice de inferência: contagem de loops while cuja condição de teste é
diferente de True.
3.3.2 – Loops For
Como descrito anteriormente, deve-se dar preferência ao uso do loop for sobre o
loop while ao se iterar sobre um conjunto de elementos, devido à sua sintaxe mais limpa
e menos susceptível a erros por parte do programador, e devido à sua maior eficiência
em geral. Em certos casos, o programador pode desejar modificar o conjunto sobre o
qual se está iterando durante o loop, adicionando ou removendo elementos. Esse tipo de
manipulação costuma gerar um comportamento do for pouco intuitivo para o
programador, podendo resultar até mesmo em um loop infinito. Logo, neste caso em
específico, é preferível iterar sobre uma cópia deste conjunto.
Está ilustrado a seguir um exemplo de como esta prática pode vir a ser
24
maliciosa:
#Código
example_list = [1]
for i in example_list:
example_list.append(0)
Neste exemplo, a cada iteração do loop um novo elemento é adicionado ao fim
da lista, o que faz com que o interpretador rode uma iteração adicional deste loop para o
novo elemento (0), repetindo esse processo por tempo indefinido. O mesmo aconteceria
se o elemento fosse adicionado ao início da lista, sendo a única diferença que a variável
i tomaria sempre o valor do mesmo elemento (1).
Proposta de índice de inferência: contagem de loops for que modificam o conjunto
sobre o qual se está iterando.
3.3.3 – Geração de Threads
Multithreading é a capacidade de um processo de criar várias linhas de execução
(threads) concorrentes. Estes threads geralmente são utilizados para executar tarefas em
paralelo, com o objetivo de otimizar o uso da CPU, seja através do uso de seus
múltiplos núcleos, ou do aproveitamento de seu tempo ocioso devido a tarefas que
requerem um tempo de espera, como o acesso a disco. Em relação ao uso de múltiplos
processos, multithreading tem a vantagem de que todos os threads compartilham os
dados do processo, e portanto eles ocupam no total menos espaço na memória do
computador.
Em Python, multithreading pode ser obtido através da derivação da classe
Thread, pertencente ao módulo threading, reescrevendo na subclasse as funções de
inicialização(__init__) e execução (run). Essa classe possui as estruturas clássicas de
gestão de concorrência, como semáforos e locks. Além disso Python possui também o
módulo Queue que implementa três tipos diferentes de fila (FIFO, LIFO e Heap de
prioridade), projetadas para o uso em um contexto de produtores e consumidores,
25
especialmente úteis em programação com threads.
Apesar deste foco no paralelismo, dependendo do interpretador, múltiplos
threads não conseguirão atuar simultaneamente devido à “global interpreter lock”
(GIL) [10]. Se trata de um semáforo interno do interpretador que sincroniza a execução
dos threads, de modo a permitir que apenas um deles execute em um determinado
momento. E a cada determinado número de instruções executadas ou quando o thread
ativo é bloqueado, a GIL é liberada para que um outro thread possa executar.
Isso faz com que o uso de threads represente um ganho de desempenho apenas
quando se deseja paralelizar processos que utilizem operações de I/O, como acesso à
disco ou a transferência de informações da rede. Porque, por serem lentas, estas
operações bloqueariam a execução do thread ativo até que os dados fiquem prontos para
ser processados, e liberaria a GIL para que um thread que estivesse em espera pudesse
executar. Desta maneira a invocação de uma operação de I/O não implicaria no bloqueio
do processo como um todo. Caso o intuito da utilização de múltiplos threads seja fazer
o aproveitamento dos múltiplos núcleos do processador, aconselha-se a utilização do
módulo multiprocessing [11].
Em relação à geração dos threads, não existe um modo simples de determinar a
quantidade de threads que devem ser gerados em um programa. Isso depende de
diversos fatores, como a quantidade de memória disponível para o processo e o tempo
médio que o thread passará bloqueado. Mas independentemente da quantidade de
threads que serão gerados, uma boa prática é utilizar um loop for para a sua geração,
pois como já descrito nas seções anteriores, este tipo de loop realiza uma quantidade
fixa e bem definida de iterações, evitando a geração infinita de threads.
Proposta de índice de inferência: Contar a quantidade de operações de I/O dentro de
classes que estendam threads.
26
3.3.4 – Acesso a Recursos Externos
Define-se aqui como recursos externos, tudo aquilo o que não faz parte do
enviado pelo usuário para o servidor e que possa ser consumido como recurso
computacional. Isto inclui desde dados armazenados no próprio servidor, seja em uma
base de dados ou no próprio sistema de arquivos em disco, até serviços online.
Este tipo de recurso pode ser especialmente perigoso quando sua fonte é
desconhecida ou não confiável. Isso se mostra claro quando é analisado o principal
mecanismo de infecção de vírus atualmente, os droppers. Tratam-se de programas
simples, que conseguem passar desapercebidos em uma varredura de um programa
antivírus, e cuja única função é instalar o vírus propriamente dito, que pode já estar
contido no dropper ou que será baixado durante a sua execução.
Logo o mau uso deste tipo de recurso pode ser uma potencial fonte de infecção
ao servidor, com efeitos indesejáveis, tais como: dar o controle do servidor para um
atacante, permitir a sua utilização para atacar outras máquinas da rede, ou derrubar
serviços do servidor, entre outros. Desta forma, recomenda-se nunca utilizar um recurso
de fonte não confiável, o que no caso em questão, pode ser algo externo ao domínio
cern.ch. Deve-se também ter cautela ao se utilizar recursos cujas fontes sejam
confiáveis, pois nem sempre se pode garantir que estes recursos se encontram livres de
infecção. Em outras palavras, a utilização deste tipo de recurso deve ser feita apenas
quando extremamente necessário.
Proposta de índice de inferência:
- Contagem de importação de módulos que não sejam parte da biblioteca padrão ou
não estejam no mesmo pacote do módulo que está sendo rodado. Uma abordagem pode
ser a realização de um scan recursivo em todos os módulos que estiverem sendo
importados.
- Contagem de uso de funções que façam pedidos à recursos externos, como requests
http e acesso à sockets.
27
3.3.5 – Saltos incondicionais
Em Python não existe um comando goto ou jump, que represente um salto
incondicional para uma determinada linha do código. Isso pode ser considerado um
benefício da linguagem, já que na maioria das vezes esse tipo de salto torna o código
mais confuso e portanto, aumenta a chance de inserção de erros durante a sua escrita ou
durante revisões.
Na verdade, em Python, como em outras linguagens usam-se majoritariamente
saltos condicionais, como o if, para se fazer o controle do fluxo de execução do
programa. E existem instruções que causam saltos incondicionais como o continue e o
break, que são formas mais restritas do goto, usadas quando for necessário parar uma
ou todas as iterações de um loop de maneira abrupta. Ambos estes comandos, apesar de
substituíveis, podem as vezes tornar o código mais legível, porém devem ser utilizados
com cautela. Como indicação geral para a sua boa utilização, eles devem ser
empregados junto a saltos condicionais, como condição de parada de um loop.
Proposta de índice de inferência: quantidade de saltos incondicionais dividido pelo
número de linhas de cada loop.
3.3.6 – Comunicação entre processos
A comunicação entre processos é feita quando se deseja transmitir dados entre
diferentes processos. Ela pode ser realizada, entre processos em uma mesma máquina,
ou entre máquinas diferentes através da rede. Esta seção vai abordar principalmente o
caso de processos em uma mesma máquina, e vai focar na utilização do módulo
multiprocessing, por motivos já explicados na seção de Geração de Threads.
A necessidade de usar múltiplos processos em um único programa surge quando
se tem acesso a diversos núcleos de processamento e deseja-se distribuir tarefas entre
eles para otimizar o uso de recursos da máquina. Uma das maneiras mais comuns de se
fazer a distribuição destes recursos é o modelo Produtor-Consumidor, no qual um ou
mais processos produtores geram dados que serão consumidos pelos processos
consumidores. Um exemplo de uso deste modelo poderia ser um caso aonde existem
28
processos (produtores) que buscam informação em diferentes bancos de dados e a
disponibilizam para os consumidores tratarem.
Para a implementação deste modelo, alguns detalhes referentes à concorrência
devem ser considerados. O primeiro deles é a necessidade de haver exclusão mútua ao
se acessar o buffer dos “produtos”. O segundo detalhe é garantir que não ocorrerão erros
ou deadlocks quando um produtor tenta adicionar um item a um buffer cheio, ou quando
um consumidor tenta retirar um item de um buffer vazio.
Ambos esses problemas podem ser resolvidos através do uso de semáforos e
monitores, porém a implementação é complicada e pode levar programadores
inexperientes a cometer erros. Para evitar tal tipo de problema, recomenda-se usar a
classe queue do módulo multiprocessing, pois ela já possui essas manipulações de
semáforo encapsuladas em suas funções put e get, tornando seu uso transparente ao
programador.
Mas nem sempre se busca um comportamento do tipo Produtor-Consumidor
entre os processos. Pode ser que para uma determinada aplicação seja necessário o uso
de pipes, ou de algum módulo que faça a chamada de um método remoto como o
xmlrpc. Se esse for o caso, deve-se sempre utilizar algum limite de tempo para o
recebimento da resposta (timeout). Isso evita que problemas de sincronização, como a
perda de uma mensagem ou a terminação de um dos processos, resultem em um
processo eternamente bloqueado, esperando uma resposta.
Por último, vale ressaltar que chamadas a processos remotos se enquadram no
caso de acesso a recurso externo, logo devem ser feitas respeitando as recomendações
da Seção 3.3.4. Um bom exemplo desta necessidade é o próprio módulo xmlrpc antes
mencionado, que na própria documentação do Python [14] possui um aviso para
possíveis vulnerabilidades que podem ser exploradas por dados construídos de maneira
maliciosa.
Proposta de índice de inferência:
- Contagem do uso explícito de semáforos e monitores.
- Verificar se está sendo utilizado um tempo de timeout em funções que façam a troca
29
de mensagens.
3.3.7 – Acesso a Portas
Uma porta é um ponto virtual utilizado para transmitir e receber dados entre
diferentes processos em um mesmo computador, ou entre diferentes máquinas na rede.
O principal modo de utilização de portas é o modelo cliente/servidor, no qual uma
aplicação é dividida em dois programas, um consumidor de serviços e outro produtor
dos mesmos. O primeiro tem como papel fazer a requisição de um ou mais serviços e o
segundo se mantém em espera, e uma vez que essa requisição seja feita ele a executa.
Este tipo de acesso é considerado um acesso a recurso externo e portanto possui
as mesmas vulnerabilidades apresentadas na seção de Acesso a Recursos Externos,
porém possui uma característica adicional. Além dele permitir a utilização de dados
potencialmente não seguros, quando utilizado como servidor, o programa também
possibilita que fontes exteriores, potencialmente desconhecidas, façam chamadas aos
serviços disponibilizados no servidor. Tal cenário pode dar a um hacker, a possibilidade
de utilizar essas chamadas para fazer uma grande variedade de ataques, indo desde a
injeção de código no servidor, até a utilização do servidor para fazer um ataque de
negação de serviços (DoS) em outras máquinas.
Proposta de índice de inferência: Quantidade de funções que fazem/disponibilizam o
acesso às portas.
3.3.8 – Acesso a Recursos Lentos
Pela própria natureza do uso de scripts, é muito provável que durante sua
execução será feito algum acesso a um recurso tido como lento, em outras palavras, um
recurso cuja velocidade de resposta é consideravelmente menor do que a do
processador. Uma das ocorrências mais comuns deste tipo de acesso é a requisição de
dados, de uma base de dados em disco ou em um servidor remoto, que serão
posteriormente utilizados no programa.
Mas apesar de frequentemente utilizado, o acesso à esse tipo de recurso deve ser
30
feito com cautela, pois ao ser requisitado, o sistema operacional bloqueia esse processo
até que ele esteja pronto para prosseguir. Sabendo disso, é importante que no seu uso
seja definido um tempo máximo de espera (timeout). Porque caso ocorra uma falha em
algum componente, que impeça o envio de resposta a solicitação de um recurso lento, o
processo que o solicitou poderá ficar eternamente bloqueado na memória do
computador, esperando a resposta.
Além disso, outros cuidados devem ser tomados, como evitar ativamente que um
mesmo recurso seja solicitado múltiplas vezes durante a execução de um script, através
do seu armazenamento em memória. Outro cuidado seria utilizar múltiplas threads
quando necessário fazer múltiplas requisições a recursos independentes, para otimizar o
uso do tempo livre do processador, como descrito na Seção 3.3.3.
Proposta de índice de inferência:
- Contar a quantidade de acessos redundantes a recursos lentos que poderiam ter sido
armazenados na memória.
- Verificar qual/se o tempo de timeout está sendo usado a cada acesso a recurso de
rede.
3.3.9 – Reflexão
Reflexão é a habilidade que um programa tem de manipular como dados algo
que represente seu estado durante a sua própria execução [12]. Este tipo de habilidade
possui diversas utilidades, como a de adaptar um programa a diferentes situações de
maneira dinâmica ou a de inspecionar variáveis, classes e etc, em tempo de execução.
Contudo, uma funcionalidade como esta de elevado grau de liberdade para o
programa, representa também um risco em potencial quando utilizada para gerar código
em tempo de execução. Isso fica evidente quando se analisa a vulnerabilidade mais
comum em aplicações web, a injeção de código, que é frequentemente causada pelo uso
de dados de entrada de usuário para escrever código que será avaliado em tempo de
execução.
Apesar de ataques maliciosos estarem fora do escopo deste trabalho, ainda assim
31
se aconselha evitar o uso de funções como eval e exec. Tais descuidos ao tratar dados
utilizados na geração dinâmica de código podem implicar um comportamento errático e
imprevisível do programa.
Proposta de índice de inferência: Contagem de chamada a funções que avaliam e
executam código dinamicamente.
3.4 – O Loop For
O caso do loop for foi escolhido para ser abordado neste trabalho, pois ele
apresenta diversas particularidades interessantes, que demandam uma análise completa
e serão apresentadas na Seção 3.5. Além disso, uma vez bem definidas as abordagens
para este caso, tem-se como resultado uma aplicação cuja expansão para tratar os outros
casos pode ser feita de maneira simples. Esta seção visa explicar as particularidades do
funcionamento de um loop for em Python, para determinar quais características a
aplicação deve possuir.
O for, por definição, aceita um conjunto de variáveis sobre as quais serão
atribuídos valores a cada iteração e um objeto iterável, de onde, em geral, são retirados
estes valores. No Python qualquer objeto pode ser aceito como iterável desde que ele
possua definida uma função com o nome __iter__, que será invocada no início da
execução do for, e deve retornar um objeto que será utilizado como iterador.
Este objeto é responsável por determinar qual valor o for utilizará em cada
iteração do loop. A condição para que um objeto funcione como iterador, é que ele
tenha definida a função next (ou __next__ no caso de Python 3). Esta função é
invocada a cada iteração do loop e deve retornar o próximo valor a ser utilizado, ou
levantar uma exceção “StopIteration” caso o loop tenha chegado ao fim.
Exemplo:
class CustomListIterator:
def __init__(self, elements):
self.elements = elements
self.iteration_index = -1
32
def next(self):
self.iteration_index += 1
if self.iteration_index >= len(self.elements):
raise StopIteration
else:
return self.elements[self.iteration_index]
class CustomList:
def __init__(self, elements=[]):
self.elements = elements
def __iter__(self):
return CustomListIterator(self.elements)
def append(self, value):
self.elements.append(value)
a = CustomList([1, 2, 3])
for i in a:
print i
else:
a.append(i + 1)
A classe “CustomList” deste exemplo foi construída para imitar o
funcionamento de uma lista ao ser iterada. Quando executado este programa teria o
seguinte funcionamento ao atingir o loop for:
1- Inicialmente o for invoca a função __iter__ do objeto a e armazena uma
referência para o valor retornado, no caso um objeto do tipo
“CustomListIterator”.
2- É invocada a função next do objeto armazenado em 1.
3- Se esta função levantar uma exceção do tipo “StopIteration”, o loop
termina, pulando para o passo 6. Caso contrário ele atribui a i o valor
retornado no passo 2.
33
4- São executadas as instruções do corpo do loop, imprimindo o valor de i.
5- Retorna-se para o passo 2, começando uma nova iteração.
6- Executam-se as instruções do else, adicionando ao final da lista de elementos
de a o último valor atribuído a i, incrementado de 1.
Neste exemplo o for poderia se manter eternamente em loop, se em seu corpo
houvesse alguma instrução que a cada iteração adicionasse um novo elemento a lista de
elementos de a, como por exemplo a.append(0). Se isso tivesse ocorrido, a função
next, invocada no passo 2, jamais levantaria uma exceção, pois a cada nova iteração a
lista de elementos de a seria maior em um elemento.
3.4.1 – Conjuntos de objetos
Na biblioteca padrão de Python existem diversas estruturas de dados que podem
ser iteradas por um loop for. Dentre elas as mais comumente utilizados são as listas,
tuplas e strings, que são sequencias ordenadas de objetos. Para este projeto foram
observadas as maneiras de se alterar a quantidade de elementos das listas, pois das três
estruturas antes mencionadas esta é a única dinâmica.
Isso quer dizer que quando se tenta adicionar um novo elemento a uma tupla ou
a uma string, o interpretador cria um novo objeto que contém a concatenação dos
valores antigos e novos. Já no caso das listas, o interpretador consegue adicionar novos
elementos sem precisar gerar um novo objeto. Devido a esse comportamento, não
ocorrem erros de loop infinito ao se iterar sobre tuplas e strings, pois o objeto iterado
referido pelo for (obtido no passo 1 do exemplo da Seção 3.4) não é o mesmo que o
referido pela variável após sua modificação.
Quanto as formas de se adicionar objetos a uma lista, existem duas maneiras que
podem gerar um loop infinito quando feitas dentro de um for. A primeira é através da
utilização do operador composto de soma e atribuição, representado pelos caracteres
mais e igual (+=). Este operador, no caso das listas, acrescenta todos os elementos de
uma lista presente à sua direita, à lista que está sendo operada, localizada à sua
esquerda. Como por exemplo:
Sendo a = [1, 2, 3] e b = [4, 5].
34
Ao se executar a instrução a += b, teríamos:
a = [1, 2, 3, 4, 5]
A segunda maneira é através das funções insert, append e extend, específicas
da classe lista. Estas funções quando invocadas como atributo de um objeto l do tipo
lista (por exemplo l.append(atr)), inserem em l o atributo (atr) que lhe foi passado
como argumento. Como por exemplo:
Sendo a = [1, 2, 3] e b = [4, 5].
Ao se executar a instrução a.append(b), teríamos:
a = [1, 2, 3, [4, 5]]
Já a instrução a.extend(b), aplicada sobre o mesmo caso inicial resultaria em:
a = [1, 2, 3, 4, 5]
E por fim a instrução a.insert(i, b) inseriria o valor de b na posição i, então para:
i = 1 teríamos a = [1, [4, 5], 2, 3]
Nota-se que o resultado da operação “a += b” é igual ao da chamada de função
“a.extend(b)”. Isso ocorre pois o que o operador “+=” faz no caso das listas, é
essencialmente, chamar a função extend, como pode ser observado no código fonte do
interpretador cpython[16].
3.5 – Desafios da Análise Estática
Segundo descritos na Seção 3.4.1, a aplicação produzida neste trabalho tem
como função encontrar instruções que fazem o uso do operador “+=” ou das funções
insert, append e extend sobre um objeto que está sendo iterado em um for. Vale
ressaltar aqui que no caso das listas, uma instrução do tipo “a += b” não equivale a “a =
a + b”. Pois, apesar de ambas gerarem em a uma lista que concatena a lista a e a lista b,
a primeira adiciona ao objeto que estava referenciado em a todos os elementos da lista
b, atuando essencialmente como a função extend. Já a segunda gera um novo objeto
que possui todos os elementos de a e todos os elementos de b, e o atribui a a, de modo
que o objeto referenciado em a se torna diferente do que havia sido armazenado por um
loop for, não configurando assim um loop infinito.
Estas instruções em um primeiro momento podem parecer bem simples de serem
35
encontradas em uma análise estática, consistindo apenas de uma verificação rápida por
declarações do tipo “iter += lista” ou “iter.append(atr)”. Porém o problema é mais
complexo do que aparenta, pois estas declarações podem ser feitas através de uma outra
variável que referencia o objeto iterado ou dentro de uma chamada de função que
possua referência a esse objeto, seja esta através do uso de uma variável global ou tendo
sido passada como argumento.
Exemplo:
def foo(l):
l.append(0)
a = [1, 2, 3]
for i in a:
foo(a)
No exemplo acima, a busca de uma instrução da forma “a.append(atr)” não
resultaria em nada. Porém a instrução “l.append(0)” é executada durante chamada da
função foo, localizada dentro do for. Como nessa chamada de função a é passado como
argumento, ao qual é atribuído o nome l, qualquer modificação imposta a l é também
imposta a a, pois ambos referenciam o mesmo objeto. Logo, neste exemplo existe uma
infração a uma das regras, apesar de append nunca ter sido chamado diretamente para a.
A linguagem Python oferece ainda um desafio adicional por causa de sua
tipagem dinâmica, que em alguns casos, torna impossível determinar estaticamente o
tipo da variável que está sendo operada em determinadas instruções do código. Esta
incerteza faz com que a análise estática seja especialmente difícil quando se faz a
chamada de uma função, pois uma instrução do tipo “obj.foo()” pode executar funções
diferentes dependendo do tipo de objeto “obj”.
A seguir, este exemplo de polimorfismo fica mais claro. Nele o objeto “obj”
pode ser uma instancia de “obj_1” ou “obj_2” dependendo do valor da variável
“condition”, e portanto ao se fazer a chamada de função “obj.foo()” o valor impresso
na tela poderá ser 1 ou 2.
Exemplo:
36
class obj_1:
def foo(self):
print 1
class obj_2:
def foo(self):
print 2
if condition:
obj = obj_1()
else:
obj = obj_2()
obj.foo()Capítulo 4
Implementação
Ao se considerar os desafios apresentados na Seção 3.5, fica claro que técnicas
simples de leitura do código não seriam suficientes para encontrar as formas mais
complexas do fato observado que deve ser tratado. Por causa disso, foi estabelecido que
a aplicação aqui implementada funcionaria de maneira similar a um interpretador,
mantendo em memória representações dos objetos que seriam gerados ao se executar o
código que está sendo analisado, e um dicionário contendo as referências de cada
variável.
Esta aplicação possui uma classe principal responsável pela gestão do contexto
das variáveis (GlobalContext), que recebe como entrada a AST do programa a ser
analisado. Esta classe é capaz de ler cada um dos tipos de nós de instrução presentes na
AST e fazer as devidas manipulações no dicionário de variáveis. A aplicação possui
ainda classes especializadas para a leitura de instruções em contextos locais como o de
chamada de funções e declaração de classes.
Uma visão geral das classes da aplicação está presente na Figura 4.1, e estas
serão exploradas um pouco mais a fundo nas seções deste capítulo.
37
Figura 4.1: Diagrama de Classes Simplificado
4.1 – Determinando o Tipo de Uma Variável
O primeiro dos desafios, mencionados na Seção 3.5, que a aplicação deve tratar
é o da tipagem dinâmica. Ao se analisar a árvore sintática, o único momento em que se
é possível determinar o tipo de uma variável é quando se encontra um nó de atribuição
(Assign). Este nó possui um atributo value que contém o valor da atribuição e um
atributo target, que consiste de uma lista de todos os alvos aos quais o valor será
atribuído.
Quando surge um nó Assign, o interpretador inicializa a variável caso ela ainda
não tenha sido inicializada e aponta a sua referência para o objeto que lhe foi atribuído.
Logo, se for possível determinar o tipo de value, podendo ele ser um tipo nativo, ou
uma referência a uma variável cujo tipo já é conhecido, o gestor de contexto é capaz de
determinar e atualizar o tipo das variáveis alvo da atribuição, deixando de lado os tipos
que ela possuía anteriormente.
O único problema desta abordagem é que no caso de atribuições feitas dentro do
corpo de estruturas de controle de fluxo, como if/else, não é possível determinar
estaticamente se elas serão executadas ou não. Quando estas bifurcações no fluxo do
programa ocorrem, é criada uma cópia do contexto atual para cada uma das possíveis
alternativas. Estas cópias seguem lendo as instruções de seu respectivo caminho e ao
final elas são fundidas, resultando na possibilidade de uma variável ter múltiplos tipos.
4.2 – Os Tipos
Na árvore sintática gerada pelo módulo ast, existem 6 tipos nativos para Python
2.x:
- Num, que pode representar um inteiro, float ou número complexo e tem o seu
valor armazenado em seu atributo n. Ex: O float 1.5 geraria o nó Num(n=1.5)
- Str, que representa uma sequência de caracteres e tem o seu valor armazenado
no atributo s. Ex: A string “texto” geraria o nó Str(s='texto').
- List e Tuple, representam respectivamente listas e tuplas, que são grupos de
objetos ordenados, podendo eles serem dinâmicos no caso das listas, ou estáticos no
38
caso da tupla. Ambos os tipos de nós possuem o atributo elts, que é uma lista
responsável por armazenar os nós dos elementos que estão contidos na lista, e o atributo
ctx, que assume o valor de um nó Store se a lista ou tupla for o alvo de uma atribuição,
ou o valor de um nó Load caso contrário. Ex: A lista [1, “texto”] geraria um nó
List(elts=[Num(n=1), Str(s='texto')], ctx=Load()).
- Set, representa estruturas que armazenam coleções desordenados de objetos
únicos. Similar as listas e tuplas, o nó de um set possui um atributo elts para armazenar
seus elementos, mas não possui o atributo ctx, pois nunca é utilizado como alvo de uma
atribuição. Ex: O set {1, “texto”} geraria um nó Set(elts=[Num(n=1), Str(s='texto')]).
- Dict, representa os dicionários da biblioteca padrão de Python. Assim como as
listas, tuplas e sets, os dicionários funcionam como containers, mas em vez de
possuírem uma lista de elementos, eles possuem os atributos keys e values, que
representam respectivamente uma lista de chaves e uma lista de seus valores
correspondentes. Ex: O dicionário {1: “texto_1”, 2: “texto_2”} geraria um nó
Dict(keys=[Num(n=1), Num(n=2)], values=[Str(s='texto_1'), Str(s='texto_2')]).
Para representar os tipos nativos, foram criadas as classes TypeObject e
Container. Elas possuem o atributo object_type, que consiste de uma string com o
nome do tipo nativo escrito em letras minúsculas e precedido pelo caractere jogo da
velha. Este atributo é utilizado como uma “etiqueta” que informa o tipo de objeto que as
instancias dessas classes representam.
Além dos tipos nativos, foram considerados ainda 3 outros tipos, as funções, as
classes e os objetos, e para representa-los foram criadas respectivamente as classes
Function, CustomClass e CustomObject. Estas classes, além do atributo object_type,
possuem atributos e funções específicos, como uma lista de argumentos de uma função,
ou uma função que busca um atributo de um objeto, considerando fatores como a sua
herança.
4.3 – Variáveis e Containers
Para se armazenar os objetos que contém os possíveis tipos de cada variável,
apresentados na Seção 4.2, foi criada a classe “Variable”. Seu atributo content consiste
de uma lista de todos os tipos que podem ter sido atribuídos à variável que ela
39
representa. Os objetos que implementam a classe “Variable” são os objetos utilizados
no dicionário de variáveis do gestor de contexto.
Existe ainda a classe “Container”, mencionada na Seção 4.2. É uma
especialização da classe “Variable”, para os tipos nativos de Python que comportam
múltiplos elementos, como as listas e tuplas. Um objeto que instancia esta classe, tem
um funcionamento dual de “Variable” e “TypeObject”, possuindo portanto um
atributo object_type que indica o seu tipo, e um atributo content que consiste de uma
lista de objetos “Variable” e “Container” que ele referencia.
4.4 – Gestores de Contexto
A classe GlobalContext e suas classes filhas foram inspiradas no mecanismo de
controle dos registros de ativação presente nas linguagens de programação. Elas
funcionam de forma similar a um interpretador, sendo capaz de ler sequencialmente as
instruções de um programa, codificadas em uma árvore de sintaxe pelo módulo ast do
Python, e tratar cada tipo de instrução (atribuição, loop for, loop while e etc) segundo
o que será descrito posteriormente nesta seção.
Ao ler uma instrução, estas classes tem como principal objetivo reconhecer
atribuições e os tipos que estão sendo amarrados. Em seguida, elas atualizam o seu
dicionário de variáveis, com os resultados da atribuição. Este último, consiste de um
dicionário da biblioteca padrão de Python, no qual as chaves são strings com o nome
das variáveis e o valor correspondente a cada chave é um objeto do tipo “Variable”.
A principal classe gestora de contexto, o “GlobalContext”, é responsável por
gerir o contexto global do programa. Ela é a conhecedora de todas as variáveis globais e
possui funções para o tratamento dos diferentes tipos de nós da árvore sintática. A
leitura de qualquer instrução do corpo do programa é feita utilizando este contexto, e
cabe a ele gerar outros contextos se necessário.
As classes derivadas da “LocalContext”, são responsáveis por gerir o contexto
dentro das chamadas de função e declaração de classes. Como elas derivam
indiretamente da classe “GlobalContext”, elas possuem todas as funções de tratamento
de instruções, sendo algumas sobrescritas para ter comportamento específico de
40
contextos locais. Elas possuem ainda referências para o contexto global e para o
contexto que as gerou, podendo ele ser local ou global. Estas referências são necessárias
caso alguma das instruções utilize um nome que não está definido no contexto corrente,
como por exemplo, uma chamada de uma função definida no contexto global, e é feita
dentro de uma outra chamada de função.
O modo como os gestores de contexto tratam os principais tipos de instrução
será explicado nas subseções a seguir.
4.4.1 – Atribuição (Assign)
Tipo de nó responsável pela definição dos tipos das variáveis. Ao ser encontrado
são avaliados as suas propriedades value e targets, correspondentes respectivamente ao
valor a ser atribuído e a uma lista das variáveis às quais este valor será atribuído. Após
avaliadas ambas as propriedades, as variáveis, determinadas pelo atributo targets, têm
sua lista de tipos atualizada no dicionário de variáveis, com os tipos encontrados ao se
avaliar o atributo value.
4.4.2 – Controle de Fluxo
Foram consideradas instruções de controle de fluxo, todas as instruções que
podem implicar em mais de um fluxo de execução no programa, como por exemplo as
instruções de if e for. Para tratar tais instruções o gestor de contexto cria uma cópia de
si, para cada um dos possíveis caminhos que o programa pode tomar ao executar tal
instrução. Em seguida cada cópia executa as instruções pertencentes ao fluxo para o
qual ela foi criada. E no final cada uma das cópias é fundida ao contexto inicial, sendo
as listas contendo os tipos de cada uma das variáveis concatenadas.
Exemplo:
if condition: a = 5 else: a = "string"
Para tratar a instrução do exemplo acima, o gestor de contexto bifurcaria a sua
execução em duas frentes, criando uma cópia de si mesmo para tratar o fluxo do
programa que passaria pelas instruções contidas na parte do if (a = 5), e outra para tratar
41
as instruções contidas no else. Após o término de ambos os fluxos a primeira cópia do
gestor de contexto consideraria a como sendo um tipo numérico, e para a segunda cópia,
a seria do tipo string. Ao chegar neste ponto o gestor de contexto faria a fusão dos dois
resultados, atualizando o seu dicionário de variáveis para considerar que a pode ser
tanto do tipo numérico, quanto do tipo string.
4.4.3 – Definição de Função
Ao analisar uma definição de função, o gestor de contexto cria um objeto do tipo
“Function” para conter o código e os argumentos da função, e o armazena em seu
dicionário de variáveis. A rotina para tratar as definições de função, por si só não realiza
grandes alterações no estado do programa, porém ela é um passo essencial para o
tratamento das chamadas de função.
4.4.4 – Chamada de Função
A análise de uma chamada de função começa com a determinação de qual
função está sendo chamada. Uma vez determinada qual função deverá ser analisada, é
criado um objeto instanciando a classe “FunctionContext”, que será responsável por
tratar todas as particularidades de uma chamada de função, como a gestão dos
argumentos e das variáveis globais, bem como pela execução do código presente no
corpo da função.
Uma vez terminada a execução do contexto da função, são atualizadas no
contexto global todas as alterações que foram feitas às variáveis globais e é retornada
uma lista com todos os possíveis tipos de retorno da função. Caso haja mais de uma
possibilidade de função a ser executada no primeiro passo, o programa trata todas as
opções como se estivesse tratando um caso de múltiplos fluxos. Neste caso ele criaria
um “FunctionContext” para cada uma das possíveis funções a serem executadas, e
fundiria os resultados no final.
4.4.5 - Definição de Classe
A análise de uma definição de classe se assemelha em muito à de uma chamada
de função. Ele começa com a criação de um gestor de contexto do tipo “ClassContext”,
especializado no tratamento de definições de classe. Então são extraídas todas as
variáveis locais deste gestor de contexto, para criar um objeto do tipo “CustomClass”
42
que servirá para representar o tipo da classe no contexto que ela foi definida.
Finalmente, todas as variáveis globais que foram usadas na definição da classe são
também atualizadas no dicionário de variáveis do “GlobalContext”.
Figura 4.2: Diagrama de Classes Detalhado
43
4.5 – Referências
Um dos possíveis casos mencionados anteriormente, no qual se torna difícil
determinar estaticamente se há ou não modificação de uma variável var, é quando esta
modificação é feita indiretamente, usando o nome de uma outra variável que referencia
o mesmo objeto que var. Por exemplo, se duas variáveis a e b estiverem referenciando
uma mesma lista e a for escolhida como objeto iterável de um loop, qualquer
modificação feita em b no corpo deste loop será feita também em a, configurando um
possível caso de erro.
Exemplo:
a = [1, 2, 3]
b = a
for i in a:
b += [0]
A solução para este possível problema veio naturalmente com o design da
aplicação. Como os tipos são determinados quando há atribuições, e são representados
como objetos, ao se identificar uma atribuição “a = b”, as referências a todos os
“TypeObjects” de a são copiadas para a lista de tipos de b. Deste modo, uma procura
no conteúdo de duas variáveis a e b por objetos comuns permite determinar se duas
variáveis referenciam um mesmo objeto.
4.6 – Implementando a Regra do Loop For
Com a implementação do gestor de contexto terminada, torna-se mais simples
implementar classes capazes de inferir se alguma regra está sendo quebrada. Basta
estender a classe “GlobalContext” e reescrever as funções que leem os tipos de nós
específicos a nova regra.
Desta forma, as seguintes regras devem ser aplicadas quando analisando o corpo
de um loop for:
1. Não devem existir operações do tipo “+=” que têm como alvo o objeto iterado
pelo for.
44
2. Não devem ser feitas invocações das funções insert, append e extend do objeto
iterado pelo loop for.
Em ambos os casos, as regra só podem ser avaliadas ao se ler uma instrução que
está dentro de um loop for, portanto em chamadas aninhadas dentro da função que
avalia um nó do tipo for (read_for). Este fato faz com que seja necessário disponibilizar
para todas as funções de leitura de nós, quais são os objetos iterados no momento de sua
execução, sem que o cabeçalho das funções seja modificado.
Para que este comportamento fosse obtido, foi adicionada a classe uma lista de
objetos iterados, desta forma qualquer função pode acessá-la. Além disso, a função
read_for foi modificada para que no início de sua execução tais objetos fossem
adicionados ao final da lista antes mencionada, e uma vez finalizada a leitura de seu
corpo, eles fossem removidos. Desta forma, além de garantir que as regras sejam
avaliadas somente dentro do corpo do for, é garantido também que as regras serão
propriamente avaliadas no caso de múltiplos for aninhados.
Uma vez modificado o read_for, as funções que leem os nós que podem
quebrar as regras já possuem os meios necessários para aferir tais ocorrências. Para a
primeira regra, a função em questão é a read_aug_assign. Ela foi modificada para que
toda vez que encontrar um nó cujo operador for “+=”, seja feita uma verificação se o
seu alvo faz referência a algum dos elementos da lista de objetos iterados. Se isso
acontecer o contador de infrações é incrementado.
Exemplo:
# Código
a = [1, 2, 3]
for i in a:
a += [0]
# AST
For(target=Name(id='i',ctx=Store()),
iter=Name(id='a',ctx=Load()),
body=[AugAssign(target=Name(id='a',ctx=Store()),
45
op=Add(),
value=List(elts=[Num(n=0)], ctx=Load()))],
orelse=[]
)
Ao se avaliar o for do código acima, a função read_for inicialmente avaliaria o
atributo iter do nó, extraindo todos os objetos aos quais ele pode corresponder, no caso
apenas um objeto lista, e os adiciona à lista de objetos iterados. Em seguida ele
começaria a avaliar o corpo do for, cuja única instrução é “a += [0]”. Como o nó desta
instrução é do tipo “AugAssign”, a função read_aug_assign é invocada.
Esta função por sua vez, avaliza o operador da instrução e determina que ele é
do tipo “+=”, então ela avalia se algum dos possíveis objetos, aos quais seu alvo pode
corresponder, encontra-se na lista de objetos iterados. Quando descobre que isso é
verdade, ela incrementa o contador de infrações de regra e continua sua execução.
Finalmente, ao final da execução da função read_for o único objeto que estava na lista
de objetos iterados é removido, de modo a impedir que posteriores instruções similares
a “a += lista” sejam avaliadas como infrações.
Para a segunda regra, a função capaz de avalia-la é a read_call, que faz a leitura
das chamadas de função. Esta é modificada de maneira semelhante ao que foi feito na
função read_aug_assign. Sempre que a função invocada for uma das três mencionadas
na regra (append, insert e extend), verifica-se se o objeto ao qual pertence essa
chamada encontra-se na lista de objetos iterados. Caso isso ocorra, configura-se também
uma quebra de regra, e o contador de infrações é novamente incrementado.
Esta abordagem, apesar de prática possui um empecilho. Ao se estender a classe
“GlobalContext”, as modificações feitas em suas funções não são automaticamente
aplicadas às instâncias da classe “LocalContext” que forem geradas por ela. Por isso,
foi necessário que a base da classe “LocalContext” fosse determinada dinamicamente.
Para que esse funcionamento fosse alcançado, utilizou-se como artifício o padrão de
projeto “Factory”, no qual a classe é definida localmente, dentro de uma função e as
suas bases são passadas como argumento, e no final a função retorna a classe construída
dentro de sua execução.
46
Exemplo:
def makeLocalContext(bases):
class LocalContext(bases):
# ---- funções da classe ----
# ...
return LocalContext
4.7 –Testes de Sanidade
Como para esse projeto não estava disponível uma base de testes, foram gerados
um total de 12 scripts que contém combinações dos seguintes casos de erro previstos:
1- Uso do operador “+=”, tendo como alvo o objeto iterado por um loop for.
2- Uso das funções insert, append e extend de um objeto iterado por um loop for.
3- Casos 1 e 2 utilizando uma variável que referência um objeto iterado por um
loop for. Por exemplo:
a = [1, 2, 3]
b = a
for i in a:
b += [0]
4- Casos 1 e 2, sendo a variável uma variável global de uma chamada função feita
dentro de um loop for. Por exemplo:
def f():
global a
a += [0]
a = [1, 2, 3]
for i in a:
f()
5- Casos 1 e 2, tendo a variável sido passada por um argumento de uma função. Por
exemplo:
47
def f(v):
v += [0]
a = [1, 2, 3]
for i in a:
f(a)
6- Casos 1 e 2 contendo múltiplos for aninhados. Por exemplo:
a = [1, 2, 3]
b = [0, 0, 0]
for i in a:
for j in b:
a += [0]
7- Casos 4 e 5 com múltiplas chamadas de função aninhadas. Por exemplo:
def f1():
global a
f2(a)
def f2(v):
v += [0]
a = [1, 2, 3]
for i in a:
f1()
No Anexo A são apresentados estes 12 scripts utilizados na base de testes, e
ainda, estes scripts contém 62 eventos de erro a serem identificados. A aplicação de
análise dos scripts, uma vez executada, foi capaz de identificar os erros com 100% de
aproveitamento.
48
Capítulo 5
Conclusão
5.1 – Conclusões
Apesar de o experimento ter sido feito com um conjunto de testes limitado e de
apenas ter sido implementado o analisador de um dos fatos, pode-se concluir que a
extração dos parâmetros descritos na Seção 3.3 é possível, mesmo com todos os
desafios impostos pelo tipo de análise que deve ser conduzido e pela linguagem. Além
disso, a abordagem utilizada neste trabalho se mostrou capaz de extrair grandes
quantidades de informações implícitas no código devido a seu funcionamento similar ao
de um interpretador. Porém, por ser tratar de uma análise estática, esta aplicação ainda é
incapaz de ler todo tipo de construção dinâmica de código ou importação dinâmica de
módulos.
Com isso, as técnicas aqui descritas poderiam ser utilizadas para a criação de
uma aplicação capaz de orientar desenvolvedores para a criação de códigos de maior
qualidade. Poderiam ainda ser integradas aos analisadores de códigos já existentes,
permitindo que eles sejam capazes de fazer análises mais robustas e de implementar
regras mais complexas.
5.2 – Trabalhos Futuros
A princípio, seria importante para esta aplicação que fosse implementada a
leitura das instruções dos tipos try e import, que foram deixadas de lado, e os outros
índices de inferência, para que uma análise mais completa de sua viabilidade pudesse
ser feita. Ficam delegados também a trabalhos futuros o cálculo do índice de
periculosidade de um programa a partir dos índices de inferência obtidos da avaliação
49
das diferentes regras, e a elaboração de um conjunto de testes mais rigoroso, que possa
botar a aplicação realmente a prova.
Existindo uma versão mais completa da aplicação descrita neste trabalho, ela
pode vir a ser útil em qualquer projeto que necessite fazer o uso da análise estática de
um programa em Python, como por exemplo projetos nas áreas de Segurança e
Qualidade de Software. Por isso seria interessante ter uma maneira mais simples de
escrever uma regra de inferência, possivelmente em uma linguagem especial, similar ao
que foi feito por Seifert e Samlaus utilizando OCL [6]. Isso permitiria a adoção desta
aplicação de maneira mais rápida, sem necessitar conhecer a fundo o seu
funcionamento.
Existem ainda dois experimentos que poderiam aumentar a precisão das análises
da aplicação. O primeiro seria a criação de um analisador análogo ao aqui descrito para
a biblioteca padrão de Python, escrita em C, para que ela pudesse integrar o
funcionamento desta aos seus testes. E o segundo seria tentar inserir este tipo de análise
em um interpretador Python, de modo a analisar cada instrução, logo antes de executá-
la. Isso tornaria a aplicação capaz de ler as construções dinâmicas mencionadas
anteriormente, sendo ainda capaz de evitar a execução de código defeituoso ou
malicioso.
Finalmente, seria importante criar uma extensa base de testes, contendo tanto
programas defeituosos quanto programas livres de falhas. Esta seria utilizada para fazer
a análise da eficiência das ferramentas que virão a ser desenvolvidas.
50
Bibliografia
[1] Python Software Foundation, “What is Python? Executive Summary,” 2017.
[Online]. Available: https://www.python.org/doc/essays/blurb/. [Acesso em 31
Maio 2017].
[2] PyScience-Brasil, "Python: O que é? Por que usar?," [Online]. Available:
http://pyscience-brasil.wikidot.com/python:python-oq-e-pq. [Accessed 14 9 2017].
[3] Python Software Foundation, “Comparing Python to Other Languages,” 1997.
[Online]. Available: https://www.python.org/doc/essays/comparisons/. [Acesso em
31 Maio 2017].
[4] W3Techs, “Usage Statistics and Market Share of Server-side Programming
Languages for Websites, May 2017,” 2017. [Online]. Available:
https://w3techs.com/technologies/overview/programming_language/all. [Acesso em
10 Maio 2017].
[5] GCN, “Static vs dynamic code analysis,” [Online]. Available:
https://gcn.com/articles/2009/02/09/static-vs-dynamic-code-analysis.aspx. [Acesso
em 14 Setembro 2017].
[6] Mirko Seifert and Roland Samlaus. "Static Source Code Analysis using OCL",
Proceedings of the 8th International Workshop on OCL Concepts and Tools (OCL
2008) at MoDELS 2008, Electronic Communications of the EASST, 2008. doi:
10.14279/tuj.eceasst.15.174
[7] Yao-Wen Huang; Fang Yu; Christian Hang; Chung-Hung Tsai; D. T. Lee; Sy-Yen
Kuo. "Securing Web Application Code by Static Analysis and Runtime Protection",
In: WWW Proceedings of the 13th international conference on World Wide Web ,
New York, p.17-22, 2004. doi: 10.1145/988672.988679
51
[8] A. A. S. Gomes, "Inteligência Computacional na Avaliação de Códigos em um
Sistema Complexo de Detecção com Desenvolvimento Colaborativo," Dissertação
de Mestrado em Engenharia Elétrica, Universidade Federal do Rio de Janeiro, Rio
de Janeiro, 2016.
[9] Python.org, “Python Patterns - An Optimization Anecdote,” Python Software
Foundation, [Online]. Available: https://www.python.org/doc/essays/list2str/.
[Acesso em 20 Junho 2017].
[10] D. Beazley, “Understanding the Python GIL,” em PyCon 2010, Atlanta, Georgia,
2010.
[11] Python Software Foundation, “16.6 multiprocesing - Process-based "threading"
interface - Python 2.7.13 documentation,” 2017. [Online]. Available:
https://docs.python.org/2/library/multiprocessing.html. [Acesso em 23 Junho 2017].
[12] J. M. e. al, "A Tutorial on Behavioral Reflection and its Implementation," Montréal,
Quebec, Canada, 1996.
[13] OWASP, “Static Code Analysis,” [Online]. Available:
https://www.owasp.org/index.php/Static_Code_Analysis. [Acesso em 05 Agosto
2017].
[14] Python Software Foundation, “20.23. xmlrpclib — XML-RPC client access,”
[Online]. Available: https://docs.python.org/2/library/xmlrpclib.html#module-
xmlrpclib. [Acesso em 11 Agosto 2017].
[15] Steve Holden (Chairman da PyCon, 2003 a 2005), “While Loop,” [Online].
Available: https://wiki.python.org/moin/WhileLoop. [Acesso em 08 Agosto 2017].
[16] Python Software Foundation, “cpython,” [Online]. Available:
https://hg.python.org/cpython/file/db842f730432/Objects/listobject.c#l914. [Acesso
em 11 Agosto 2017].
[17] J. Dahse, “RIPS - A static source code analyser for vulnerabilities in PHP scripts,”
the Month of PHP Security, 24 Maio 2010.
52
[18] N. Jovanovic, C. Kruegel, and E. Kirda, “Pixy: a static analysis tool for detecting
Web application vulnerabilities,” 2006 IEEE Symposium on Security and Privacy
(S&P’06), 2006. doi: 10.1109/sp.2006.29
[19] Python Software Foundation, “32.1. parser — Access Python parse trees,” [Online].
Available: https://docs.python.org/2/library/parser.html. [Acesso em 14 Maio
2017].
[20] Johann C. Rocholl, Florent Xicluan and Ian Lee, “Introduction — pycodestyle
2.3.1 documentation,” [Online]. Available:
http://pycodestyle.pycqa.org/en/latest/intro.html. [Acesso em 14 Setembro 2017].
[21] Python Software Foundation, “32.7. tokenize — Tokenizer for Python source,”
[Online]. Available: https://docs.python.org/2/library/tokenize.html. [Acesso em 14
Maio 2017].
[22] T. Kluyver, "Green Tree Snakes - the missing Python AST docs," [Online].
Available: https://greentreesnakes.readthedocs.io/en/latest/. [Accessed 14 Setembro
2017].
[23] L. Rauchwerger, “CSCE 434-500: Compiler Design,” 2017. [Online]. Available:
https://parasol.tamu.edu/~rwerger/Courses/434/lec6.pdf.
53
Anexo A
Base de Testes
Teste 1
a = [1, 2, 3]
for i in a:
a += [4] #Erro
a.append(4) #Erro
a.insert(4,1) #Erro
a.extend([4]) #Erro
Teste dos casos base de loop infinito do for.
Teste 2
a = [1, 2, 3]
b = a
for i in a:
b += [4] #Erro
b.append(4) #Erro
b.insert(4,1) #Erro
b.extend([4]) #Erro
Teste dos casos base de loop infinito do for, usando uma variável b que
referencia o mesmo objeto que a variável a que foi passada para o for.
Teste 3
def f():
global a
global b
54
a += [4] #Erro
a.append(4) #Erro
a.insert(4,1) #Erro
a.extend([4]) #Erro
b += [4] #Erro
b.append(4) #Erro
b.insert(4,1) #Erro
b.extend([4]) #Erro
a = [1, 2, 3]
b = a
for i in a:
f()
Teste dos casos base de loop infinito do for, usando variáveis globais de uma
função invocada dentro do for. Nota-se aqui que foram testados os casos tanto da
variável a que foi passada para o for, quanto o da variável b, que referencia o mesmo
objeto que a.
Teste 4
def f(v):
v += [4] #Erro para f(a) e f(b)
v.append(4) #Erro para f(a) e f(b)
v.insert(4,1) #Erro para f(a) e f(b)
v.extend([4]) #Erro para f(a) e f(b)
a = [1, 2, 3]
b = a
for i in a:
f(a)
f(b)
Teste dos casos base de loop infinito do for, no qual o objeto iterado é passado
como argumento para uma função invocada dentro do for.
55
Teste 5
def f(a):
a += [4] #Erro para f(a)
a.append(4) #Erro para f(a)
a.insert(4,1) #Erro para f(a)
a.extend([4]) #Erro para f(a)
a = [1, 2, 3]
b = [1, 2, 3]
for i in a:
f(a)
f(b)
Este teste apesar de parecer redundante em relação ao teste 4, tem como objetivo
verificar que a aplicação não está interpretando a variável a dentro da função f, como
uma variável global.
Teste 6
def f1():
f2()
def f2():
global a, b
a += [4] #Erro
a.append(4) #Erro
a.insert(4,1) #Erro
a.extend([4]) #Erro
b += [4]
b.append(4)
b.insert(4,1)
b.extend([4])
a = [1, 2, 3]
b = [1, 2, 3]
56
for i in a:
f1()
Teste para garantir que a regra é verificada mesmo com chamadas de função
aninhadas.
Teste 7
def f1(v):
f2(v)
def f2(v):
v += [4] #Erro para f1(a)
v.append(4) #Erro para f1(a)
v.insert(4,1) #Erro para f1(a)
v.extend([4]) #Erro para f1(a)
a = [1, 2, 3]
b = [1, 2, 3]
for i in a:
f1(a)
f1(b)
Mistura do teste 4 com o teste 6, garante que a regra é verificada em chamadas
de função aninhadas, nas quais o objeto iterado é passado como argumento para f1 e
sucessivamente para f2.
Teste 8
def f1():
global a, b
f2(a)
f2(b)
def f2(v):
v += [4] #Erro para f2(a)
v.append(4) #Erro para f2(a)
57
v.insert(4,1) #Erro para f2(a)
v.extend([4]) #Erro para f2(a)
a = [1, 2, 3]
b = [1, 2, 3]
for i in a:
f1()
Mistura do teste 3 com o teste 6, garante que a regra é verificada em chamadas
de função aninhadas, nas quais o objeto iterado é obtido em f1 como uma variável
global e é passado como argumento para f2.
Teste 9
a = [1, 2, 3]
b = [1, 2, 3]
for i in a:
b += [4]
for j in b:
a += [4] #Erro
a.append(4) #Erro
a.insert(4,1) #Erro
a.extend([4]) #Erro
b += [4] #Erro
Teste para garantir que na ocorrência de múltiplos loops for aninhados, a regra é
verificada para o for exterior.
Teste 10
a = [1, 2, 3]
b = [1, 2, 3]
c = a
d = b
for i in a:
d += [4]
58
for j in b:
c += [4] #Erro
c.append(4) #Erro
c.insert(4,1) #Erro
c.extend([4]) #Erro
d += [4] #Erro
Mistura do teste 9, com o teste 2. Garante que a regra é verificada em loops
aninhados, mesmo quando se utilizam variáveis que referenciam o objeto iterado pelo
loop.
Teste 11
def f2():
global a, b
a += [4] #Erro
a.append(4) #Erro
a.insert(4,1) #Erro
a.extend([4]) #Erro
b += [4]
b.append(4)
b.insert(4,1)
b.extend([4])
def f1(v):
for i in v:
f2()
a = [1, 2, 3]
b = [1, 2, 3]
f1(a)
Teste que garante a execução da verificação, mesmo quando o for encontra-se
em contexto local.
59
Teste 12
def f1(v):
for i in v:
f2()
def f2():
global a, b
a += [4] #Erro
a.append(4) #Erro
a.insert(4,1) #Erro
a.extend([4]) #Erro
b += [4] #Erro
b.append(4) #Erro
b.insert(4,1) #Erro
b.extend([4]) #Erro
a = [1, 2, 3]
b = [1, 2, 3]
for i in b:
f1(a)
Mistura de diversos testes com loops e funções aninhadas alternadamente.