Buenas Prácticas de Escritura de Código

Juan Vera del Campo - juan.vera@professor.universidadviu.com

Como decíamos ayer...

  • El modelado de amenazas permite identificar amenazas y tratarlas en el momento del diseño
  • ¿Podemos mejorar la seguridad de una aplicación simplemente escribiendo buen código?

Hoy hablamos de...

  1. Errores de código más comunes
  2. Buenas prácticas
  3. Gestión de usuarios
  4. Proyectos, librerías y dependencias
  5. VibeCoding with Artificial Intelligence
  6. Referencias

Errores de código más comunes

OWASP: Top 10

center

https://owasp.org/Top10/2025/0x00_2025-Introduction/

center

  1. Broken access control: usuarios capaces de hacer cosas a las que no deberían estar autorizados. Defensa: denegación por defecto
  2. Security Misconfiguration Defensa: system hardening
  3. Software Supply Chain Failures: uso de librerías vulnerables. Defensa: auditorías
  4. Cryptographic failures: protección definiciente de los datos en tránsito y almacenados. Defensa: políticas de datos
  5. Injection: el usuario puede forzar la ejecución de comandos. Defensa: librerías especializadas, validación de entradas
  1. Diseño inseguro. Defensa: Threat Modeling.
  2. Authentication failures. Atacantes entrando por fuerza bruta. Ejemplo de defensa: 2FA
  3. Integrity Failures No comprobar qué estamos ejecutando
  4. Monitoring Failures Errores en la gestión de alertas y logs
  5. Exceptional Conditions Errores en la gestión de los propios errores

Otros proyectos

OWASP tiene otros proyectos

Otras recomendaciones: SANS Top25

center

https://www.sans.org/top25-software-errors/

Otras recomendaciones: MITRE

Rank ID Name
[1] CWE-79 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
[2] CWE-787 Out-of-bounds Write
[3] CWE-89 Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
[4] CWE-352 Cross-Site Request Forgery (CSRF)
[5] CWE-22 Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
[6] CWE-125 Out-of-bounds Read
[7] CWE-78 Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
[8] CWE-416 Use After Free
[9] CWE-862 Missing Authorization
[10] CWE-434 Unrestricted Upload of File with Dangerous Type

https://cwe.mitre.org/top25/archive/2024/2024_cwe_top25.html

Buenas prácticas

Recomendaciones

  1. Input Validation: nunca te fíes de tus entradas
    • Tamaño de la entrada
    • Caracteres válidos
    • Formato, tipo (entero, cadena...) -> Sanitizar
    • Dentro de valores permitidos (máximos, mínimos...)
    • ¡Existente!
  2. Manejo de secretos
    • Comprueba que los errores de autenticación no incluyen información
    • Guarda las contraseñas de forma segura en todos los dispositivos
    • Transmite las contraseñas de forma segura
    • https://github.com/OWASP/wrongsecrets
  1. Least privilege
    • Valida los permisos en cada petición
    • Crea tests que validan permisos
    • Mantén las autorizaciones en el nivel mínimo posible
  2. Gestión de librerías seguras y probadas
  3. Usable
  4. Acceso denegado por defecto
  5. Registra todas las acciones (logging)
  6. Usa guías de estilo (linters)
  7. Control de calidad (ver siguiente temas)

https://codesigningstore.com/secure-coding-practices-to-implement

Input validation

Siempre tienes que "sanitizar" cualquier input del usuario

import os

filename = input('Select a file for deletion: ')
os.system('rm %s')

¿Qué pasa si el usario introduce -rf / ; dd if=/dev/random of=/dev/sda ?

Mejor:

import os

filename = input('Select a file for deletion: ')
os.remove(filename)

Pero...

¿Qué pasa si el usario introduce * ?

Input validation: injection

Ejemplo con SQL injection:

txtUserId = getRequestString("UserId");
txtSQL = "SELECT * FROM Users WHERE UserId = " + txtUserId;

Si, por ejemplo, UserId=5, el comando devolverá los datos del usuario 5

Si un atacante es capaz de asignar UserId= "105 OR 1=1"...

Se ejecutará: SELECT * FROM Users WHERE UserId = 105 OR 1=1;

¡Este comando devuelve toda la base de datos!

https://www.w3schools.com/sql/sql_injection.asp

Registra todas las acciones

No uses print(), sinó el módulo logging (Java: log4j)

import logging

logger = logging.getLogger()
logger.warning('Esto es un mensaje de warning %d', 5)
logger.info('Esto es un mensaje de info')
logger.info('Esto es un mensaje de error')

Estas librerías especializadas permiten configurar la salida de log. Por ejemplo: errores consola y archivo, info solo a archivo, colores, incluir fechas...

https://cwe.mitre.org/data/definitions/778.html

https://www.geeksforgeeks.org/logging-in-python/

Guías de estilo (linters)

  • Tienes que entender el código de otro para poder decidir si es seguro o no
  • En dos semanas, no entenderás tu propio código
  • Los lenguajes que dan mucha libertad pueden provocar que cada programador escriba de una manera
  • Todos los programadores del equipo deben seguir las mismas reglas

Ejemplos:

center

Gestión de usuarios

Broken Access

Cosas a tener en cuenta

  • No guardes contraseñase en claro en la base de datos
  • No guardes la contraseña de conexión a la base de datos en el código
  • Comprueba si los usuarios tienen autorización para realizar las acciones
  • Equivocarse es fácil: escribe las cosas solo una vez y reutiliza código
  • Aprovecha los mecanismos que ya incluyen las librerías que uses

Ejemplo inicial

DB_CONFIG = {
    'user': 'tu_usuario_mysql',       # e.g., 'root'
    'password': 'tu_contraseña_mysql', # e.g., 'password123'
    'host': '127.0.0.1',             # e.g., 'localhost'
    'database': 'test_python_db'     # Un nombre de base de datos existente o que deseas crear
}

@app.get("/create")
async def create(name, password):
    conn = mysql.connector.connect(**DB_CONFIG)
    sql = f'INSERT INTO users(name,password) VALUES("{name}","{password}")'
    cur = conn.cursor()
    cur.execute(sql)
    conn.commit()
    return {"message": f"I created user {name}"}

Problemas:

DB_CONFIG = {
    'user': 'tu_usuario_mysql',       # e.g., 'root'
    'password': 'tu_contraseña_mysql', # e.g., 'password123'
    'host': '127.0.0.1',             # e.g., 'localhost'
    'database': 'test_python_db'     # Un nombre de base de datos existente o que deseas crear
}

@app.get("/create")
async def create(name, password):
    conn = mysql.connector.connect(**DB_CONFIG)
    sql = 'INSERT INTO users(name,password) VALUES(?,?)'
    hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
    cur = conn.cursor()
    cur.execute(sql, (name, hashed))
    conn.commit()
    return {"message": f"I created user {name}"}

Problemas:

from fastapi import FastAPI
import sqlite3
import bcrypt

app = FastAPI()

class User:
    def __init__(self, name, password):
        self.name = name
        self.hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
    
    def check(self, password):
        return bcrypt.checkpw(password.encode, self.hashed)
    
    def create(self):
        # TODO: check uniqueness
        conn = sqlite3.connect('users.db')
        sql = 'INSERT INTO users(name,password) VALUES(?,?)'
        cur = conn.cursor()
        cur.execute(sql, (self.name, self.hashed))
        conn.commit()

@app.get("/create")
async def create(name, password):
    user = User(name, password)
    user.create()
    return {"message": f"I created user {name}"}

Problemas:

@app.get("/create")
async def create_user(
        name: str = Query(max_length=50, default=None),
        password: str = Query(min_length=3, max_length=50, default=0),
        age: int = Path(title="The age of the new user", ge=18, default=0)):
    user = User(name, password)
    user.create()
    return {"message": f"I created user {name}"}
def get_current_username(
    credentials: Annotated[HTTPBasicCredentials, Depends(security)]
):
    current_username_bytes = credentials.username.encode("utf8")
    correct_username_bytes = b"stanleyjobson"
    is_correct_username = secrets.compare_digest(
        current_username_bytes, correct_username_bytes
    )
    current_password_bytes = credentials.password.encode("utf8")
    correct_password_bytes = b"swordfish"
    is_correct_password = secrets.compare_digest(
        current_password_bytes, correct_password_bytes
    )
    if not (is_correct_username and is_correct_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect email or password",
            headers={"WWW-Authenticate": "Basic"},
        )
    return credentials.username

https://fastapi.tiangolo.com/advanced/security/http-basic-auth/#__tabbed_3_1

Gestión de usuarios logueados

@app.route('/secret_page')
def secret_page():
    if g.user is None:
            return redirect(url_for('login', next=request.url))
    pass

@app.route('/another_secret_page')
def another_secret_page():
    if g.user is None:
            return redirect(url_for('login', next=request.url))
    pass

Uso de decoradores para escribir la lógica solo una vez:

from functools import wraps
from flask import g, request, redirect, url_for

def login_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if g.user is None:
            return redirect(url_for('login', next=request.url))
        return f(*args, **kwargs)
    return decorated_function

@app.route('/secret_page')
@login_required
def secret_page():
    pass

@app.route('/another_secret_page')
@login_required
def another_secret_page():
    pass

Contraseñas en archivos de configuración

# settings.py
import os
from os.path import join, dirname
from dotenv import load_dotenv

dotenv_path = join(dirname(__file__), '.env')
load_dotenv(dotenv_path)

SECRET_KEY = os.environ.get("SECRET_KEY")
DATABASE_PASSWORD = os.environ.get("DATABASE_PASSWORD")
  • Cuidado: estos archivos de configuración no deben estar en el control de versiones
  • Considera leer de memoria: DATABASE_PASSWORD = os.environ.get("DATABASE_PASSWORD")
  • Conexión a la base de datos dentro del código: https://cwe.mitre.org/data/definitions/540.html

Secure by default

La configuración por defecto tiene que ser "acceso prohibido"

Ejemplo:

Complete Mediation

Least Privilege

Proyectos, librerías y dependencias

Supply Chain Attacks

Creación del proyecto

En esta sección crearemos un proyecto Vue directamente dentro de un docker. Esta no es la manera recomendada de crear proyectos Vue, pero lo haremos así para no tener que instalar más cosas en nuestros PCs.

docker run -ti --rm node bash
mkdir project ; cd project
npm install vue
npm install vuetify
npm install vuetify@^3.0.1
npm install eslint
ls node_modules
npm audit

Archivo package.json (JavaScript)

  • Incluye metadados del proyecto
  • Lista dependencias, con las versiones exactas
  • Ventajas: es reproducible en cualquier PC
  • Desventajas: ocupa mucho más espacio en disco

center

Ejemplos:

Archivo Package y Package.lock (Python)

"biplist": {
    "hashes": [
    "sha256:4c0549764c5fe50b28042ec21aa2e14fe1a2224e239a1dae77d9e7f3932aa4c6"
    ],
    "index": "pypi",
    "version": "==1.0.3"
  },

¡No re-inventes la rueda!

  • No re-inventes la rueda: utiliza librerías reconocidas siempre que puedas
  • Pero recuerda auditar tus librerías
  • Considera utilizar contextos para la aplicación:
    • JavaScript: npm / yarn
    • Python: pip / conda / pipenv

Ejemplo: Pytoileur

center

  • Librería publicada en Pypi el 27 de mayo de 2024 (Link)
  • Descripción genérica "pystob allows for API Authorization Management and allows to build REST-Based API with custom JSON syntax. "

Historia completa: https://www.sonatype.com/blog/pypi-crypto-stealer-targets-windows-users-revives-malware-campaign

Si descargas el código, solo tiene un archivo setup.py, que parece completamente vacío y limpio a primera vista...

center

Pero esa línea 17 en realidad oculta otro comando después de muchos espacios. Para poder verlo hay que activar el "word wrap" en tu editor de texto (Alt+Z en VS)

center

Si analizamos qué hace este código: añade un archivo a Windows que se ejecuta siempre que el usuario abre una nueva sesión en su PC

center

Análisis en Cyberchef

El atacante recomendaba instalar su librería en respuestas en sourceforge (comentarios borrados actualmente)

center

Audita tus librerías / dependencias

No te fies de nadie...

center h=20em

VibeCoding with Artificial Inteligence

Recomendaciones

  1. Tú eres el desarrollador, la IA es ayudante. La responsabilidad será tuya
  2. Aplica las Best Practices siempre: code reviews, testing, static analysis, documentation, and version control
  3. Piensa en la seguridad desde el principio
  4. Guía a la IA
  5. Pide a la IA que ella misma mejore el trabajo: diseño incremental
  6. Pregunta tus dudas y expresa tus inquietudes

https://best.openssf.org/Security-Focused-Guide-for-AI-Code-Assistant-Instructions

Workflow

  1. Haz el modelado de amenazas, prioriza y detecta requisitos de seguridad
  2. Diseña la arquitectura del sistema, con o sin ayuda
  3. Escribe los pasos anteriores en un documento. Ejemplo: CLAUDE.md
  4. Deja que la IA escriba el sistema
  5. Diseño iterativo:
    • Asegúrate de que todo funciona
    • Añade tests unitarios
    • Instrucciones de uso de control de versiones
    • Actualiza documentación

Ejemplo: https://github.com/Juanvvc/passmanager2

Referencias

Para practicar: Bwapp

  • Aplicación web con múltiples problemas de seguridad
  • ¡No incluye explicaciones!
  • Tres niveles de dificultad

http://www.itsecgames.com/

  1. docker run --rm -p 8080:80 raesene/bwapp
  2. Go to: http://localhost:8080/install.php and click on "install"

Para practicar: WebGoat

  • Web de aprendizaje de OWASP
  • Incluye lecciones y ejercicios de la lista de OWASP

https://owasp.org/www-project-webgoat/

  1. docker run --rm -p 8080:8080 -p 9090:9090 -t -e TZ=Europe/Amsterdam webgoat/goatandwolf
  2. Ve a http://localhost:8080/WebGoat y registra un nuevo usuario

Ejemplo 1

function authenticateUsers(username, password) {
    var accounts = apiService.sql("SELECT * FROM users");
    for(var i=0, i<accounts.length; i++) {
        var account = account[i];
        if(account.username === username && account.password === password) {
            return true;
        }
        if ("true" === "true") {
            return false;
        }
    }
}
$("#login").click(function () {
    var username = $("#username").val();
    var password = $("#password").val();
    var authenticated = authenticatedUser(username, password);
    if (authenticated === true) {
        $.cookie('loggedin', 'yes', {expires: 1});
    } else if (authenticated === false) {
        $("error_message").show(LogIn Failed);
    }
});

https://twitter.com/hot_girl_spring/status/1853430439022670236

Input validation: templates

import yaml

input = '''
title: Título de Prueba
alumnos:
    - María
    - Eva
    - Alberto
    - Jorge
'''

clase = yaml.load(input, yaml.BaseLoader)
print(clase['alumnos'])

# Salida: ['María', 'Eva', 'Alberto', 'Jorge']
import yaml

input = '!!python/object/new:sys.exit [42]'
yaml.load(input, Loader=yaml.UnsafeLoader)

¿Cuál es la salida de este comando?

https://theconversation.com/what-is-log4j-a-cybersecurity-expert-explains-the-latest-internet-vulnerability-how-bad-it-is-and-whats-at-stake-173896

Valida lo que dice el usuario

center

El bug no es obvio: si file es un path absoluto, ignora los paths anteriores: https://docs.python.org/3/library/os.path.html

center

¡Gracias!

WARNING: está clase es interactiva. Las transparencias son solo una sugerencia

Ya vimos durante la sesión anterior qué es lo que hace en proyecto OWASP Entre sus guías, incluye el top ten de vulnerabilidades que debemos evitar en nuestro código

## Recomendaciones para Python - Always sanitize external data - Scan your code - Be careful when downloading packages - Review your dependency licenses - Do not use the system standard version of Python - Use Python’s capability for virtual environments - Set DEBUG = False in production - Be careful with string formatting - (De)serialize very cautiously - Use Python type annotations > https://snyk.io/blog/python-security-best-practices-cheat-sheet/

En el caso particular de esta función, no pasa nada: la función no permite atajos de shell. Pero tienes que tenerlo en cuenta para tus funciones∫

Y los logs de aplicación puedes fácilmente centralizarlos en un SIEM

Para forzar las mismas reglas en toda la empresa, puedes utilizar linters: no permitirán que un código compile o se suba a git si no sigue las reglas de la empresa PEP8 es un ejemplo de reglas. Hay muchos más. Los linters los puedes encontrar para cada lenguaje. Ejemplos en Python: pylama, frake8. Ejemplos en Javascript: eslint

--- ![center w:40em](images/coding/demos1.png) --- ![center w:25em](images/coding/demos2.png)

Esta transparencia la veremos con comandos reales, está aquí solo para referencia

Soluciones: - https://jaiguptanick.github.io/Blog/blog/Walkthrough_of_bWAPP_solutions_A1_injection_writeups/ - https://dumbmaster.blogspot.com/2017/01/owasp-top-10-bwapp-walkthrough-for-a1_21.html

Nota: no he sido capaz de ejecutar WebWolf desde el docker, quizá prefieras hacerlo en tu propio PC

This code snippet contains several issues that indicate a lack of experience, which is likely why it was shared with the caption, "I don't think the intern will last much longer." Here are some specific issues: Potential SQL Injection: The code uses apiService.sql("SELECT * FROM users") to retrieve user data. If apiService.sql doesn't implement SQL injection protection, this approach could expose the database to attacks. Typically, prepared statements or ORM (Object-Relational Mapping) tools are used to avoid this risk. Inefficient Authentication Logic: The authenticateUser function iterates through all user accounts in the database (for (var i = 0; i < accounts.length; i++)). In a real-world application, fetching all user records to find a single match is highly inefficient and poses security risks by unnecessarily loading all user data. Hardcoded Condition Always Returning False: At the end of authenticateUser, there's an if ("true" === "true") { return false; } condition, which always evaluates to true and will always return false, regardless of whether the user credentials are correct. This effectively prevents any user from logging in successfully, making the authentication function useless. Unsecure Cookie Storage: The code uses $.cookie('loggedin', 'yes', { expires: 1 }); to set a login cookie without any additional security configurations, such as HttpOnly or Secure flags. This makes the cookie vulnerable to client-side manipulation or interception in non-HTTPS contexts. Potential Incorrect Error Handling: The line $("error_message").show(LogIn Failed) appears to be missing proper syntax for displaying the error message. It should likely be $("#error_message").show("Login Failed");, where # selects the element by ID, and "Login Failed" should be wrapped in quotes. These issues are commonly made by beginners and can lead to performance problems and significant security vulnerabilities, which is likely why the post humorously suggests that "the intern won't last much longer."

Un ejemplo de esta vulnerabildad que nos tuvo varias semanas pegados a la pantalla en 2021 fue log4j