Skip to main content

Seguridad a nivel de tipos: ¿el futuro de la generación segura de código con IA?

Escrito por
blog feature snyk code green

4 de junio de 2026

0 minutos de lectura

Introducción

Como el código se escribe (y se genera) más rápido que nunca, también aparecen vulnerabilidades de seguridad a un ritmo sin precedentes. Pedirle a tu LLM que no incluya vulnerabilidades de seguridad en su código no siempre funciona. Cada vez está más claro que la forma en que se crea el software hoy, de manera manual o asistida, no basta para escribir código seguro de manera confiable, consistente y comprobable.

El auge de Rust ha demostrado que es posible eliminar por completo clases enteras de vulnerabilidades de manera práctica y ergonómica. Rust ha eliminado eficazmente todas (o casi todas) las vulnerabilidades de corrupción de memoria en tiempo de compilación. Entonces, ¿por qué limitarnos a esta clase de vulnerabilidades?

En esta publicación, explicaré cómo es posible escribir código de tal manera que las vulnerabilidades de seguridad de las aplicaciones web sean incompilables (o no puedan pasar la verificación de tipos), y cómo las bibliotecas seguras desde el diseño o los envoltorios de bibliotecas bien ubicados pueden impedir por completo que se escriban clases enteras de vulnerabilidades, ya sea manualmente o con un LLM. Mostraré patrones de código en Python y Rust que podrían usarse para eliminar clases de vulnerabilidades.

Por qué los tipos

Cuando se usan correctamente, los sistemas de tipos pueden ser herramientas increíblemente poderosas. Te permiten codificar las invariantes de un sistema; es decir, todas las reglas, propiedades y relaciones de cada dato de tu programa. En lugar de dejar un comentario útil o realizar aserciones en tiempo de ejecución (que quizá solo se activen cuando tu primer cliente intente hacer algo importante), puedes asegurarte de que cada llamada a tu función add solo pase dos tipos int, en todos los casos y en toda la base de código.

Por experiencia, el código que escribo con tipos extremadamente estrictos tiene muchos menos errores en tiempo de ejecución que el código que escribo sin ellos, porque me veo obligado a razonar sobre cada parámetro, cada dato y cada entrada. Cuando codifico todo como corresponde, cometo muchos menos errores y mi código solo tiene errores de lógica de negocio, no errores como «ups, pasé una cadena a una función que suma enteros».

Muchas vulnerabilidades de seguridad son simplemente un tipo específico de error y, si aceptamos lo que afirmé antes, muchos de esos errores deberían poder resolverse con un sistema de tipos lo suficientemente flexible.

Tipos confiables

Esta publicación se inspira, en parte, en la API web Trusted Types. Esta API mitiga eficazmente la mayoría de las formas de XSS del DOM al garantizar que los puntos de inserción de XSS solo acepten valores conocidos como seguros, como los que depura un sanitizador de XSS adecuado. Es posible reforzar aún más esta API con CSP para exigir el uso de Trusted Types, que genera un TypeError si no se usa.

El kernel de Linux usa un mecanismo similar con la macro __user, que garantiza que los punteros de espacio de usuario se manejen correctamente.

En las siguientes secciones, veremos cómo generalizar esta técnica y aplicarla a clases arbitrarias de vulnerabilidades de seguridad.

Cómo resolver IDOR

La referencia directa insegura a objetos (IDOR) es una vulnerabilidad persistente que se origina en la falta de controles de autenticación o autorización al realizar acciones mediante una API. El ejemplo clásico es un endpoint de API que recibe como parámetro un ID de usuario incremental y devuelve sus datos. Sin embargo, al cambiar el ID de usuario, devuelve los datos de otro usuario, sin los controles de autorización adecuados.

Según las estadísticas, es probable que tú, lector, uses Python, así que primero exploraremos esta vulnerabilidad y cómo resolverla en Python usando solo sugerencias de tipos.

En Python

Empecemos con un ejemplo vulnerable:

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 llamada a la API tiene un aspecto bastante estándar: pasas un ID de usuario entero, consultas la base de datos y devuelves el resultado en la respuesta. ¡Pero vaya! Olvidamos agregar controles de autenticación o autorización; cualquier usuario puede solicitar el saldo de cualquier otro usuario (si conoce su ID): un caso clásico de IDOR. Recibimos el informe de la vulnerabilidad, pagamos la recompensa al investigador y actualizamos la función de esta manera:

@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
    ...

Perfecto, los datos del usuario están seguros. Pero es un error muy fácil de cometer. Olvidar un control de autenticación en un solo endpoint de API puede tener consecuencias importantes. Ahora veamos el mismo endpoint, pero usando el sistema de tipos para asegurarnos de que esta vulnerabilidad no vuelva a ocurrir:

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"]}

Aquí hay bastante más código, pero en el panorama general quedaría integrado en tu código de autenticación y autorización.

El principal cambio en este nuevo código es que nunca pasamos tipos básicos (como el ID de usuario int de los ejemplos anteriores). Los datos de entrada se abstraen en una clase opaca a la que solo puedes acceder demostrando que ya realizaste los pasos de autenticación y autorización. Al asegurarte de que tu clase UncheckedUserID no pueda hacer nada útil (como usarse en una expresión SQL), el verificador de tipos detectará de forma confiable el error si olvidaste realizar la comprobación de autenticación para extraer el valor «real» de user_id.

Como Python es un lenguaje dinámico, técnicamente podrías omitir la comprobación y acceder al valor interno sin proporcionar un AuthenticationGuard válido, pero es de esperar que en la revisión de la solicitud de cambios se identifique ese código como indeseable. Otros lenguajes, como Rust, pueden ofrecer garantías mucho más sólidas, lo que significa que ni siquiera con código poco elegante puedes acceder directamente al valor interno de la clase envoltorio. Obviamente, el código sigue ejecutándose, así que podrías tomar medidas extremas, como volcar la memoria de la instancia. Pero si solo quieres asegurarte de que la autenticación sea consistente, ¿por qué harías algo así?

En Rust

Los especificadores de visibilidad de Rust ofrecen un control estricto sobre qué código puede acceder al valor user_id envuelto, lo que hace que esta sea una forma aún más confiable de garantizar los controles de autenticación y autorización. En el ejemplo de abajo, usamos Axum con un extractor para asegurarnos de que el valor de entrada se deserialice correctamente en nuestra clase envoltorio de seguridad.

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();
}

Viabilidad

Obviamente, este método para mitigar vulnerabilidades de seguridad requiere un compromiso considerable, sobre todo en proyectos de software ya establecidos. Se necesitaría infraestructura de desarrollo para garantizar que estos patrones se sigan de manera consistente. Las dos formas principales de lograrlo serían mediante bibliotecas envoltorio o reglas personalizadas de lint.

Tomemos como ejemplo el caso de Python. Como organización, podrías exigir el uso de la biblioteca MyOrgFastAPI, un envoltorio liviano de FastAPI, y asegurarte de que todos los endpoints de API usen tipos personalizados que cumplan estos requisitos de seguridad. Ningún endpoint debería aceptar un argumento str; tendría que ser algún tipo de UncheckedString.

Como alternativa, si no quieres mantener una biblioteca envoltorio personalizada, podrías implementar estas reglas en el linter. Aunque así no se ofrece retroalimentación al desarrollador tan pronto, sí puede brindar la misma protección al prohibir los tipos básicos.

Sea cual sea la forma de lograrlo, te asegurarías de que cada caso se maneje correctamente. Este patrón de código se puede extender de forma natural para proteger contra todo tipo de vulnerabilidades:

  • Un UncheckedString debe sanitizarse correctamente para obtener una cadena sin procesar que se devuelva al usuario, lo que mitiga XSS

  • Un UncheckedString no se puede concatenar a una consulta SQL, lo que mitiga las vulnerabilidades clásicas de SQL

  • Un UncheckedString no se puede concatenar a una cadena de comandos, lo que mitiga las vulnerabilidades de inyección de comandos.

Y la lista continúa.

Estos patrones también se aplicarían por igual a desarrolladores humanos y agentes de IA. Pedirle a un agente de IA que aplique la autenticación en todas partes quizá no sea 100 % confiable, pero exigirla en la etapa de compilación o lint puede ser muy eficaz.

Conclusión

Implementar la seguridad de esta manera, a nivel de tipos, puede ser una herramienta increíblemente poderosa tanto para desarrolladores humanos como para agentes de IA. Aunque al principio puede requerir un esfuerzo adicional, sobre todo en proyectos de software existentes, los beneficios pueden ser considerables, ya que permite eliminar clases enteras de vulnerabilidades.

La forma de manejar los datos puede variar mucho de un proyecto a otro, pero pensar en ello desde el principio puede dar grandes beneficios para crear código seguro.

DOCUMENTO TÉCNICO

La crisis de seguridad de IA en tu entorno de Python

Con el ritmo de desarrollo acelerándose a un ritmo vertiginoso, ¿sabes realmente a qué puede acceder tu entorno de IA?