Pedir código defensivo, no un exploit

Código defensivo con IA es pedir que una función compruebe lo que entra y falle en claro si no sirve. Vacío, tipo, rango. Mensajes de error útiles. No es un tutorial ofensivo. No pides exploits, recetas de ataque ni “cómo lo saltaría alguien”. Si el modelo se va por ahí, paras y reformulas.

Esta página es validación. No vuelques un .env para que “el ejemplo sea real”: las claves no entran, igual que en no pegar secretos ni el .env. Personas, DNI y salud son otro filtro. Aquí el objeto es programar asumiendo datos malos: el formulario mandará basura. Tu función no finge que el mundo es un entero limpio.

Qué pides (validar, no atacar)

Validar entradas con ChatGPT (o Claude, Gemini, el chat del editor) es un encargo estrecho. Dices qué función, qué tipo esperas, qué rechazas, cómo se avisa. El modelo escribe un borrador. Tú lees. Tú pruebas. Tú firmas.

Qué sí pides:

  • Rechazar vacío y solo espacios.
  • Comprobar tipos y vacíos: si esperas un entero, no sigas con "hola".
  • Un techo y un suelo (cero, o uno; un máximo razonable).
  • Un mensaje por fallo, en español, que el usuario o tú podáis leer.
  • Fail closed: si no pasa el chequeo, no hay valor de retorno “por defecto” silencioso. No hay 0 mágico. No hay seguir.

Qué no pides, nunca:

  • Exploits, PoCs, malware, shells, payloads.
  • Cómo “saltar” un login, un captcha, un filtro.
  • Cadenas de ataque, ejemplos de inyección, XSS, nada que se pueda copiar a un objetivo.
  • “Hazlo como un atacante para ver si aguanta.” Eso es un tutorial ofensivo con disfraz. IA no un tutorial ofensivo.

Si necesitas hablar de consultas a una base: usa parámetros / no concatene SQL. Una frase. Sin demostrar el fallo. Sin “el atacante haría…”. El detalle ofensivo no entra en el hilo.

Pedido genérico que sí:

Escribe una función parse_cantidad(texto: str) -> int.
Reglas, en este orden:
1. Si texto es None o no es cadena: error claro.
2. Si vacío o solo espacios: error claro.
3. Si no es un entero en base 10 (no floats, no mil puntos): error claro.
4. Si es menor que 1: error claro.
5. Si es mayor que 1000: error claro.
Fail closed: si falla, no devuelvas un número.
Mensajes en español, una línea, sin jerga.
No escribas exploits, payloads ni cómo saltarse la validación.
No pidas secretos. No uses claves de ejemplo realistas.

Eso es el job. El resto de esta página es el ejemplo de Inés, los límites, y qué hacer cuando el modelo se desvía.

Errores y límites de una función (antes de pegar)

Errores y límites de una función se escriben en el prompt. El modelo no conoce tu formulario. Si no le das el techo, pondrá un 999999 o no pondrá nada. Si no le das el mensaje, pondrá invalid input y un stack.

Antes de abrir el chat, anotas en cuatro líneas:

  1. Nombre y firma. parse_cantidad(texto) -> int.
  2. Qué es un éxito. Un entero de 1 a 1000 inclusive.
  3. Qué es un fallo. Vacío, no numérico, negativo o cero, demasiado grande, tipo incorrecto.
  4. Qué no cambia. No hay redondeo de "3.9" a 3. No hay aceptar "1.000" como mil si tú no lo has definido. Inés no lo ha definido: se rechaza.

Límites que no son de validación y no se piden aquí:

  • Reescribir el proyecto. Hoy: una función. Un refactor grande no entra.
  • Autenticación, tokens, .env. No hace falta una key para parsear un número.
  • “Endurecer el servidor.” Vago. Pides cheques concretos: vacío, tipo, rango.

Programar asumiendo datos malos no es cinismo. Es el martes: alguien pega un espacio, un O mayúscula en vez de cero, un -3 por un menos en el teclado. La función no “adivina”. Rechaza.

Si el dato viene de un campo de email (otro ejemplo, más abajo): asumes cadena vacía, sin @, con espacios. No asumes un buzón real. No pides herramientas de envío masivo. Formato, nada más.

Ejemplo: parse_cantidad (Inés)

El caso. Inés tiene un formulario de reservas de sala: “¿Cuántas plazas?”. El front manda texto. El back debe obtener un entero o un error. No un int() suelto que explote o, peor, que trague basura.

Inés pega el prompt del apartado anterior. Tres salidas típicas.

Intento 1 (vago: “hazla segura”).
El modelo escribe un ensayo sobre “seguridad en profundidad”, menciona ataques y ofrece “también puedo mostrarte cómo se rompería”. Paras. No sigues ese hilo. No pides el cómo. Reformulas: “Solo las cinco reglas. Cero ofensivo. Cero payloads.”

Intento 2 (demasiado listo).
Acepta "3.0", acepta " 8 " bien (eso sí), pero si falla return 0. El 0 no es fail closed. Es un número. El formulario creerá que hay cero plazas. Rechazas el return 0. Un turno:

No devuelvas 0 ni None en silencio.
Si falla, lanza ValueError con el mensaje de esa regla.
Rehaz solo parse_cantidad. Mismas reglas.

Intento 3 (comprobado). Borrador que Inés puede leer en voz alta:

def parse_cantidad(texto) -> int:
    if not isinstance(texto, str):
        raise ValueError("La cantidad debe ser texto numérico.")
    recorte = texto.strip()
    if recorte == "":
        raise ValueError("La cantidad no puede estar vacía.")
    if not recorte.isdigit():
        raise ValueError("La cantidad debe ser un entero, sin decimales ni signos.")
    n = int(recorte)
    if n < 1:
        raise ValueError("La cantidad mínima es 1.")
    if n > 1000:
        raise ValueError("La cantidad máxima es 1000.")
    return n

Compruebas, no de oído:

  1. Tipo. Un entero ya (si alguien llama mal) o None: error. No int(None).
  2. Vacío. "" y " ": error. No 0.
  3. No numérico. "abc", "3.5", "1.000", "ocho": error.
  4. Signo. "-1", "0": error. El suelo es 1.
  5. Techo. "1001": error. "1000": 1000.
  6. Éxito. "12" y " 12 ": 12.
  7. Mensajes. Cada uno apunta a esa regla. No un Error genérico que no te deja arreglar el campo.
  8. Cero ofensivo. No hay comentarios de “por si inyectan”. No hay SQL. No hay payloads.

Si el borrador concatena una consulta: lo tiras. Pides: “esta función solo parsea. No toques la base. Si más adelante hay consulta, parámetros; no concatene SQL.” Sin ejemplo de ataque.

Mensajes de error útiles se leen en el formulario. “La cantidad máxima es 1000” se puede corregir. ERR_OVERFLOW no. Pides español de España, una línea, sin ironía.

Inés no copia a producción a ciegas. Pega la función en su archivo. Lanza las llamadas a mano, en este orden, y anota el mensaje:

  1. parse_cantidad("") → error de vacío.
  2. parse_cantidad("abc") → error de entero.
  3. parse_cantidad("-1") y parse_cantidad("0") → error de mínimo o de signo.
  4. parse_cantidad("1001") → error de máximo.
  5. parse_cantidad("12") y parse_cantidad(" 12 ")12.

Si el 2 no falla, la función no sirve. Si el 5 no da 12, tampoco. El listado de casos puede pedirlo al chat; la ejecución es suya. Cómo montar la batería está en tests unitarios a partir de una función.

Comprobar tipos y vacíos (y un email sin spam)

Comprobar tipos y vacíos es el 80 % de este artículo. El resto es rango. Si saltas el vacío, el rango ni se evalúa bien.

Orden que pides, siempre el mismo (fail closed en cada paso):

  1. ¿Ha llegado el dato? None, ausente, no es el tipo.
  2. ¿Está vacío? Tras strip, longitud 0.
  3. ¿Es el tipo o el formato que has definido? Dígitos. O un email mínimo.
  4. ¿Está en rango? Mínimo, máximo. Sin “casi”.

Email, el borde correcto. Inés tiene un campo “correo de contacto de la reserva”. Pide formato, no una herramienta de correo masivo.

Función: email_reserva_ok(texto: str) -> str
Devuelve el email recortado y en minúsculas si pasa.
Reglas:
- vacío → error
- sin un solo @ → error
- nada antes o después del @ → error
- espacios en medio → error
No envíes correo. No listes destinatarios. No escribas un spammer.
No expliques cómo saltar un filtro de correo.
Mensajes en español, una línea.

Qué aceptas como “formato mínimo”: nombre@dominio.tld tosco. Qué no pides: validar si el buzón existe, SMTP, listas, “mejorar la entregabilidad”. Eso ya no es validar un campo. Si el modelo ofrece un script de envío, paras. Reformulas: “solo el string”.

Números de teléfono, DNI, IBAN: no los pegas reales al chat para “probar el validador”. Inventas "600000000" de mentira o un patrón. Documentos de identidad reales no son material de prompt. El validador se prueba con datos ficticios.

SQL, otra vez en una línea de prompt, si Inés más tarde persiste la cantidad:

El INSERT usa parámetros. No concatene SQL. No pongas ejemplos de inyección.

Nada más. Si el modelo empieza a ilustrar el fallo con una cadena rara, paras. No la copias. No la “mejoras”. Cierras el ángulo.

Si el modelo se pone ofensivo o se niega

IA no un tutorial ofensivo. A veces el modelo malinterpreta “defensivo” y ofrece el ataque para “enseñar”. A veces se niega porque has usado la palabra exploit en el título del hilo y corta de más. Los dos casos se resuelven sin pelear.

Si empieza pasos de ataque:

  1. Paras. No pides “sigue, pero más ético”.
  2. Un mensaje: “No quiero ofensivo. Solo validar vacío, tipo y rango. Fail closed. Mensajes claros.”
  3. Si el siguiente párrafo sigue siendo receta, cierras el hilo. Abres uno nuevo con el prompt de parse_cantidad. Sin la palabra exploit si eso lo dispara; el título de esta página es para ti, no tiene que ir en el chat.

Si se niega a escribir la validación porque el tema le ha sonado a ataque:

  1. Reformulas la tarea lícita: parsear un entero de un formulario.
  2. Quítale jerga de “pentest”, “bypass”, “payload”.
  3. Si tras una reformulación limpia sigue el corte, paras. No hay atajo. El criterio está en qué hacer cuando el chatbot se niega.

Qué no haces: jailbreak, “ignora tus normas”, disfrazar el mismo encargo ofensivo. Eso no es código defensivo. Es insistir.

Privacidad en este hilo. No pegas logs con tokens “para que veas el error de verdad”. El error de parse_cantidad se reproduce con "abc". No hace falta un Bearer.

Errores habituales (para aquí)

Pedir “hazla segura” sin reglas. El modelo rellena con un ensayo o con ofensivo. Para aquí: las cinco reglas de Inés, numeradas.

Aceptar return 0 o return None como fallo. No es fail closed si el llamador sigue. Para aquí: excepción o un resultado de error que el llamador tiene que mirar. Inés usa ValueError.

Redondear o “ser flexible” con "3.9" o "1.000" sin haberlo definido. La flexibilidad es un requisito nuevo. Si no está, se rechaza.

Dejar que escriba el proyecto. Una función. Si aparecen tres archivos, Docker y un login, no pegas. Vuelves a parse_cantidad.

Pedir tests y no correrlos. La lista de casos sí. El verde del runner, en tu máquina. "abc" debe fallar. "12" debe dar 12.

Pegar datos reales de clientes en el ejemplo. Un email de verdad, un DNI, una key. Fuera. Ficticios.

Concatenar SQL “temporalmente”. No. Parámetros. Sin demo de ataque.

Seguir el hilo ofensivo “para aprender a defenderte”. No en un chat de consumo. Tu trabajo es el if y el mensaje. El resto no entra.

Confiar en el mensaje en inglés que el modelo pone por defecto. Pides español y lo compruebas. El usuario del formulario de Inés no merece invalid literal for int().

Para aquí, en positivo:

  1. Reglas de vacío, tipo y rango, por escrito, antes del chat.
  2. Fail closed. Mensaje por regla.
  3. Una función. Datos ficticios.
  4. Si aparece ofensivo: paras, reformulas, o cierras.
  5. Parámetros si hay SQL. Sin payloads.

Cierre

Código defensivo con IA se reduce a esto:

  1. Escribes errores y límites de una función: vacío, tipo, rango.
  2. Validar entradas con ChatGPT: un prompt con reglas y prohibición de ofensivo.
  3. Comprobar tipos y vacíos primero. Luego el techo. Mensajes de error útiles.
  4. Programar asumiendo datos malos. "abc" no es un 12.
  5. IA no un tutorial ofensivo. Si el modelo se va, paras. Reformulas a parse_cantidad. No insistes.

Sin receta de ataque. Sin clave en el prompt. El modelo propone un if. Tú lo corres con un vacío y con un 1001.

Preguntas frecuentes

¿Qué es código defensivo cuando se lo pido a una IA?

Validar lo que entra: vacío, tipo, rango. Si algo no cumple, la función no sigue. Devuelve o lanza un error entendible. No es un tutorial de cómo romper un sistema. Es asumir que el dato vendrá mal.

¿Cómo pido a ChatGPT que valide entradas sin que se vaya a un ataque?

Nombra la función, las reglas (vacío, no número, negativo, techo) y el mensaje de cada fallo. Prohíbe exploits, payloads y ‘cómo lo saltaría alguien’. Si el modelo cambia de tema a ofensivo, paras y reformulas a validación.

¿Qué hago si el chatbot se niega o empieza a dar pasos de ataque?

Paras. No insistes en el ángulo ofensivo. Reformulas: ‘solo validación, fail closed, mensajes claros’. Si sigue el corte o el tutorial que no has pedido, cierras. El detalle de un no del modelo está en el artículo de cuándo el chatbot se niega.

¿Puedo pedir que ‘evite SQL injection’?

Puedes pedir que la consulta use parámetros y no concatene SQL. No pidas ni aceptes ejemplos de ataque, cadenas maliciosas ni ‘cómo lo haría un atacante’. El modelo no es un laboratorio. La defensa es parámetros y validar tipos; no un catálogo de payloads.

¿Hace falta que la IA escriba tests de la validación?

Puedes pedir la lista de casos (vacío, ‘abc’, ‘-1’, un número enorme, un entero válido). Los tests los corres tú. Un archivo de prueba que no has ejecutado no demuestra el límite. Eso se profundiza en tests unitarios a partir de una función.