Argumentos da Linha de Comando – Parte 1: main()

por Fabio A. Mazzarino

Na plataforma MS Windows pouco se usa a linha de comando, mas C/C++ é muito utilizado para back end, e nesse caso a linha de comando é muito mais comum. Pra começar vamos entender como a linguagem C recebe as informações do sistema operacional. Vamos também entender quais cuidados temos que ter com argumentos de linha de comando.

A linguagem C recebe os argumentos da linha de comando através de dois parâmetros na função main(). O primeiro parâmetro, um int, representa a quantidade de argumentos. O segundo parâmetro, um array de ponteiros char, cada parâmetro, separado por espaço, passado pela linha de comando, inclusive o próprio binário. Vamos a um exemplo:

#include <stdio.h>
int main(int argc, char* argv[]) {
    for (int ct = 0; ct < argc; ct++)
        printf("%s ", argv[ct]);
    printf("\n");
}

Normalmente os argumentos são nomeados argc (argument counter) e argv (argument values), mas podem receber outros nomes. O programa acima lista todos os argumentos, reconstruindo a linha de comando novamente. Note que o primeiro argumento, índice 0, é o próprio binário, incluindo caminho conforme chamado na linha de comando. Dessa forma:

$ ./args 1   2   3   4   5
./args 1 2 3 4 5

Note que apesar a linha de comando contar com vários espaços entre os diversos argumentos, a reconstrução é feita com somente um espaço. Além disso o primeiro parâmetro é reproduzido tal qual foi digitado pelo usuário.

Cuidados

Muito cuidado ao utilizar dados passados pela linha de comando por conta de alguns motivos.

Quantidade de Parâmetros

Muita atenção com a quantidade de parâmetros. Antes de indexar o array de ponteiros char procure saber se existem argumentos suficiente. Portanto sempre é boa prática validar a quantidade de argumentos:

int main(int argc, char* argv[]) {
    if (argc >= 2)
        strcpy(config.input, argv[1]);
    if (argc >= 3) {
        if (!strcmp(argv[2], "COPY")) {
            config.copy = 1;
            config.move = 0;
        } else if (!strcmp(argv[2], "MOVE")) {
            config.copy = 0;
            config.move = 1;
        } else {
            config.copy = 0;
            config.move = 0;
            strcpy(config.output, argv[2]);
        }
    }
    if (!config.output && argc >= 4)
        strcpy(config.output, argv[3]);
}

Note como é verificado se cada argumento está efetivamente disponível. Isso é necessário para evitar acessar uma espaço de memória não alocado, causando problemas e instabilidade no programa.

Tamanho dos Parâmetros

É necessário cuidado com o tamanho dos parâmetros passado pelo usuário. Durante o tempo de codificação não é possível definir qual será o tamanho do argumento passado pelo usuário. Por isso é recomendado limitar o tamanho do argumento, ou fazer a alocação do buffer em tempo de execução:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char* argv[]) {
    char name[16];
    char *path = NULL;
    if (argc < 3) {
        printf("Insufficient arguments\n");
        exit(1);
    }
    if (strlen(argv[2]) > sizeof(name) - 1) {
        printf("Maximum name size: 15 characters\n");
        exit(1);
    }
    if ((path = (char*)malloc(strlen(argv[3]) + 1)) == NULL) {
        printf("Cannot allocate enough memory\n");
        exit(1);
    }
    strcpy(path, argv[3]);
    free(path);
}

Com o primeiro argumento (argv[2]) é verificado o tamanho do argumento, se é compatível com o tamanho do buffer name, menos um caractere, necessário para completar o caracter de final de string. Também seria possível utilizar strncpy, limitando a quantidade de bytes copiados.

Com o segundo argumento (argv[3]) o buffer é alocado em tempo de execução, garantindo que haverá espaço suficiente para o argumento.

Essa validação é necessária para evitar bugs de buffer overflow, que irá causar problemas de estabilidade, mas pode abrir caminho para falhas de segurança.

Conteúdo dos Parâmetros

O conteúdo dos parâmetros passados pelo usuário é uma questão de segurança. Com um pouco de conhecimento das funcionalidades do programa é possível inserir textos maliciosos para se ter acesso a informações indevidamente, ou até mesmo destruir informações.

A questão é tão sensível que na linguagem Perl existe um termo para determinar quando os dados não foram validados: tainted, ou manchados.

É necessário validar se os argumentos recebidos não estão tentando acessar outros diretórios, ou se estão tentando executar comando maliciosos, ou ainda se pretendem adicionar comandos adicionais a comandos já existentes no código.

E neste caso não há uma receita de bolo para eliminar todos os problemas. Mas no caso de arquivos dentro de um diretório, verifique se o argumento conta com o caractere /. No caso de SQL, deve-se fazer o escape de caracteres de string: ‘. Linhas de comando precisam se certificar que não existem espaços em branco ou ;. E assim por diante.