Skip to main content

Segurança no nível de tipos: o futuro da geração segura de código com IA?

Escrito por
blog feature snyk code green

4 de junho de 2026

0 minutos de leitura

Introdução

Como o código é escrito (e gerado) cada vez mais rápido, um efeito colateral lamentável é que as vulnerabilidades de segurança também surgem mais rápido do que nunca. Pedir ao seu LLM que não inclua vulnerabilidades de segurança no código nem sempre funciona. Está cada vez mais claro que a forma como o software é criado hoje, manualmente ou com assistência, não é suficiente para escrever código seguro de maneira confiável, consistente e comprovável.

O crescimento da popularidade do Rust mostrou que é possível eliminar completamente classes inteiras de vulnerabilidades de forma prática e simples. Na prática, o Rust eliminou todas (ou quase todas) as vulnerabilidades de corrupção de memória em tempo de compilação. Então, por que deveríamos nos limitar a essa classe de vulnerabilidades?

Neste artigo, vou explicar como é possível escrever código de modo que vulnerabilidades de segurança em aplicações web sejam impossíveis de compilar (ou de passar pela verificação de tipos), e como bibliotecas seguras desde a concepção ou wrappers de biblioteca bem posicionados podem impedir completamente que classes inteiras de vulnerabilidades sejam introduzidas, manualmente ou por um LLM. Vou mostrar padrões de código em Python e Rust que podem ser usados para eliminar classes de vulnerabilidades.

Por que usar tipos?

Quando usados corretamente, os sistemas de tipos podem ser ferramentas incrivelmente poderosas. Eles permitem codificar as invariantes de um sistema, ou seja, todas as regras, propriedades e relações de cada dado do seu programa. Em vez de deixar um comentário útil ou fazer verificações em tempo de execução (que talvez só sejam acionadas quando seu primeiro cliente tentar fazer algo importante), você pode garantir que cada chamada da função add passe apenas dois valores do tipo int, em todos os casos e em toda a base de código.

Pela minha experiência, o código que escrevo com tipos extremamente rigorosos tem bem menos bugs em tempo de execução do que o código que escrevo sem eles, pois sou obrigado a raciocinar sobre cada parâmetro, cada dado e cada entrada. Quando codifico tudo isso adequadamente, cometo bem menos erros, e meu código passa a ter apenas bugs de lógica de negócio, em vez de problemas como “ops, passei uma string para uma função que soma inteiros”.

Muitas vulnerabilidades de segurança são apenas um tipo específico de bug. Se você considerar verdadeira a afirmação que fiz acima, então muitos desses bugs podem ser resolvidos com um sistema de tipos suficientemente flexível.

Tipos confiáveis

Este artigo foi inspirado, em parte, pela API web Trusted Types. Essa API mitiga, na prática, a maioria das formas de XSS no DOM, garantindo que os pontos de entrada de injeção de XSS aceitem apenas valores conhecidos como seguros, como aqueles sanitizados por um sanitizador de XSS apropriado. Também é possível restringir ainda mais essa API com CSP para exigir o uso da API Trusted Types, gerando um TypeError caso ela não seja usada.

Um mecanismo semelhante é usado pelo kernel do Linux com a macro __user, que garante que ponteiros provenientes do espaço do usuário sejam tratados adequadamente.

Nas próximas seções, vamos explorar como generalizar essa técnica e aplicá-la a classes arbitrárias de vulnerabilidades de segurança.

Como resolver IDOR

A Referência Direta Insegura a Objetos (IDOR) é uma vulnerabilidade grave que decorre da falta de verificações de autenticação e/ou autorização ao executar ações de API. Um exemplo clássico é um endpoint de API que recebe como parâmetro um ID de usuário incremental e retorna dados desse usuário. Porém, ao alterar o valor do ID, ele retorna dados de outro usuário, sem as verificações de autorização adequadas.

Estatisticamente, é provável que você, leitor, use Python. Por isso, vamos começar explorando essa vulnerabilidade e como resolvê-la em Python usando apenas dicas de tipo.

Em Python

Vamos começar com um exemplo vulnerável:

import sqlite3

from fastapi import FastAPI, Depends

app = FastAPI()

DATABASE = "app.db"

def get_db():
    conn = sqlite3.connect(DATABASE)
    conn.row_factory = sqlite3.Row
    try:
        yield conn
    finally:
        conn.close()

@app.get("/balance/{user_id}")
def get_balance(user_id: int, db: sqlite3.Connection = Depends(get_db)):
    cursor = db.execute(f"SELECT balance FROM accounts WHERE user_id = {user_id}")
    row = cursor.fetchone()
    return {"uid": user_id, "balance": row["balance"]}

Esta é uma chamada de API bem comum: você informa um ID de usuário inteiro, consulta o banco de dados e retorna o resultado na resposta. Mas, opa! Esquecemos de adicionar verificações de autenticação ou autorização; qualquer usuário pode consultar o saldo de qualquer outra pessoa (desde que tenha o ID), um caso clássico de IDOR. Recebemos o relatório da vulnerabilidade, pagamos a recompensa ao pesquisador e atualizamos a função da seguinte forma:

@app.get("/balance/{user_id}")
def get_balance(user_id: int, db: sqlite3.Connection = Depends(get_db)):
    if not (check_authentication() and check_authorization()): return 403
    ...

Perfeito, os dados do usuário estão seguros. Mas é muito fácil cometer esse erro. Esquecer uma verificação de autenticação em um único endpoint de API pode ter impactos significativos. Agora, vamos analisar o mesmo endpoint, mas usando o sistema de tipos para garantir que essa vulnerabilidade não volte a acontecer:

import sqlite3

from fastapi import FastAPI, Depends
from typing import Never, NewType, Final
from collections.abc import Generator
from contextlib import contextmanager

app = FastAPI()

DATABASE = "app.db"

def get_db():
...

UserID = NewType("UserID", int)

class AuthenticationProvider:
    def do_authn(self) -> "AuthenticationGuard":
        # do something to check the auth
        return AuthenticationGuard(guard=AuthenticationGuard._INSTANTIATION_TOKEN)  # pyright: ignore[reportPrivateUsage, reportArgumentType]

class AuthenticationGuard:
    _INSTANTIATION_TOKEN: Final = object()

    def __init__(self, *, guard: Never):
        # could also do some runtime checks to ensure caller
        assert guard is self._INSTANTIATION_TOKEN

class UncheckedUserID:
    __value: UserID

    def __init__(self, user_id: UserID):
        self.__value = user_id

    @contextmanager
    def ensure_authn(self, guard: AuthenticationGuard) -> Generator[UserID, None, None]:
        assert guard
        yield self.__value


@app.get("/balance/{user_id}")
def get_balance(unauth_user_id: UncheckedUserID = Depends(UncheckedUserID), db: sqlite3.Connection = Depends(get_db)):
    authguard = AuthenticationProvider().do_authn()
    with unauth_user_id.ensure_authn(authguard) as user_id:
        cursor = db.execute(f"SELECT balance FROM accounts WHERE user_id = {user_id}")
        row = cursor.fetchone()
        return {"uid": user_id, "balance": row["balance"]}

Há bem mais código aqui, mas, no contexto geral, ele acabaria sendo incorporado ao seu código de autenticação e autorização de qualquer forma.

A principal mudança neste novo código é que nunca usamos tipos básicos (como o ID de usuário do tipo int nos exemplos anteriores). Os dados de entrada são abstraídos em uma classe opaca, acessível apenas quando você comprova que já realizou as etapas de autenticação e autorização. Ao garantir que sua classe UncheckedUserID não possa fazer nada útil (como ser usada em uma expressão SQL), o verificador de tipos emitirá um erro de forma confiável se você esquecer de fazer a verificação de autenticação para extrair o valor “real” de user_id.

Como Python é uma linguagem tão dinâmica, tecnicamente seria possível contornar a verificação e acessar o valor interno sem fornecer um AuthenticationGuard válido. Mas esperamos que uma revisão de pull request identifique esse código indesejável. Outras linguagens, como Rust, oferecem garantias muito mais fortes: mesmo com código malfeito, não é possível acessar diretamente o valor interno da classe wrapper. É claro que o código continua em execução, então você pode tomar medidas extremas, como despejar a memória da instância. Mas, se o objetivo é apenas garantir que a autenticação seja consistente, por que faria isso?

Em Rust

Os especificadores de visibilidade do Rust oferecem um controle rigoroso sobre quais partes do código podem acessar o valor user_id encapsulado, tornando esse método ainda mais confiável para garantir as verificações de autenticação e autorização. No exemplo a seguir, usamos o Axum com um extrator para garantir que o valor de entrada seja desserializado corretamente na nossa classe wrapper de segurança.

use axum::{response::IntoResponse, routing::get, Json, Router};

mod unchecked_input {
    use axum::extract::{FromRequestParts, Path};
    #[derive(serde::Deserialize, serde::Serialize, Debug)]
    pub struct UserID(u32);

    #[derive(FromRequestParts)]
    pub struct UncheckedUserID {
        #[from_request(via(Path))]
        value: UserID,
    }

    impl UncheckedUserID {
        pub const fn ensure_authn(self, _: super::authentication::AuthenticationGuard) -> UserID {
            self.value
        }
    }
}

mod authentication {
    use std::marker::PhantomData;

    pub struct AuthenticationGuard {
        _internal: PhantomData<()>,
    }

    pub struct AuthenticationProvider {}
    impl AuthenticationProvider {
        pub const fn do_auth() -> Option<AuthenticationGuard> {
            // do something to check the auth
            Some(AuthenticationGuard {
                _internal: PhantomData,
            })
        }
    }
}

async fn get_balance(user_id: unchecked_input::UncheckedUserID) -> impl IntoResponse {
    match authentication::AuthenticationProvider::do_auth() {
        Some(guard) => {
            let user_id = user_id.ensure_authn(guard);
            // go and do some SQL
            Json(serde_json::json!({"uid": user_id, "balance": 0.0}))
        }
        None => Json(serde_json::json!({"error": "auth failure"})),
    }
}

#[tokio::main]
async fn main() {
    let app = Router::new().route("/balance/{user_id}", get(get_balance));

    let listener = tokio::net::TcpListener::bind("127.0.0.1:8000")
        .await
        .unwrap();
    axum::serve(listener, app).await.unwrap();
}

Viabilidade

É claro que mitigar vulnerabilidades de segurança dessa forma exige um comprometimento significativo, especialmente em projetos de software mais consolidados. Seria necessário criar uma infraestrutura de desenvolvimento para garantir que esses padrões sejam seguidos de forma consistente. As duas principais maneiras de fazer isso seriam com bibliotecas wrapper ou regras personalizadas de lint.

Considere, por exemplo, o caso do Python. Como organização, você poderia exigir o uso da biblioteca MyOrgFastAPI, um wrapper leve do FastAPI, e garantir que todos os endpoints de API usem tipos personalizados que implementem esses requisitos de segurança. Nenhum endpoint poderia receber um argumento str; teria que ser algum tipo de UncheckedString.

Outra opção, se você não quiser manter uma biblioteca wrapper personalizada, é implementar essas regras no linter. Embora isso não dê feedback ao desenvolvedor tão cedo, ainda pode oferecer a mesma proteção ao proibir tipos básicos.

Seja qual for o método escolhido, ele garantirá que cada caso seja tratado corretamente. Esse padrão de código pode ser estendido naturalmente para proteger contra vários tipos de vulnerabilidade:

  • Um UncheckedString precisa ser sanitizado corretamente para gerar uma string simples que possa ser retornada ao usuário, mitigando XSS

  • Um UncheckedString não pode ser concatenado a uma consulta SQL, mitigando vulnerabilidades clássicas de SQL

  • Um UncheckedString não pode ser concatenado a uma string de comando, mitigando vulnerabilidades de injeção de comandos.

E a lista continua.

Esses padrões também se aplicam tanto a desenvolvedores quanto a agentes de IA. Pedir a um agente de IA que garanta a autenticação em todos os casos talvez não seja 100% confiável, mas exigir autenticação na etapa de compilação ou lint pode ser extremamente eficaz.

Conclusão

Implementar a segurança dessa forma, no nível de tipos, pode ser uma ferramenta incrivelmente poderosa tanto para desenvolvedores quanto para agentes de IA. Embora possa haver algum esforço inicial, especialmente em projetos de software existentes, os benefícios podem ser significativos: classes inteiras de vulnerabilidades podem ser eliminadas.

A forma de tratar os dados pode variar bastante de um projeto para outro, mas pensar nisso desde o início pode trazer grandes benefícios para a segurança do código.

WHITE PAPER

A crise de segurança da IA no seu ambiente Python

Com o ritmo de desenvolvimento acelerando cada vez mais, você sabe o que o seu ambiente de IA pode acessar?