OpenCyber Academy
Grado · Capítulo 8 · Español
Código del curso: OCA-115Capítulo 08: OCA-115-C008

Capítulo 8 — Programación II

Aprendizaje gratuito y abierto para quienes desean proteger a las personas y la tecnología. No se necesita una cuenta para estudiar ni consultar las respuestas.

Mapa de la lección

Lección 08 publicada. Mapa, ejemplo, EX015.

flowchart TD
  N0["Resultados de aprendizaje y conexión"]
  N1["8.1 La entrada es una frontera de seguridad"]
  N2["8.2 Reglas de validación"]
  N3["8.4 Pruebas"]
  N4["8.6 Actividad práctica"]
  N5["Síntesis del capítulo"]
  N0 --> N1 --> N2 --> N3 --> N4 --> N5
  N5 --> EX["Ejemplo resuelto"]
  EX --> Q["Ejercicio EX015"]

Dibujo lógico

flowchart LR
  N0["Resultados de aprendizaje y conexión"]
  N1["8.1 La entrada es una frontera de seguridad"]
  N2["8.2 Reglas de validación"]
  N3["8.4 Pruebas"]
  N4["8.6 Actividad práctica"]
  N5["Síntesis del capítulo"]
  N0 --> N1 --> N2 --> N3 --> N4 --> N5 --> Q["EX015"]
[Resultados de aprendizaje y conexión]
    |
    v
[8.1 La entrada es una frontera de seguridad]
    |
    v
[8.2 Reglas de validación]
    |
    v
[8.4 Pruebas]
    |
    v
[8.6 Actividad práctica]
    |
    v
[Síntesis del capítulo]
    |
    v
[Ejemplo resuelto] --> [Ejercicio EX015]

Resultados de aprendizaje y conexión

Después de este capítulo usted podrá (1) validar entradas no confiables con reglas explícitas y rechazar lo que no se ajuste a ellas; (2) gestionar los errores para que un programa falle de forma segura y visible; (3) escribir pruebas unitarias, incluidas pruebas de casos límite y mal formados; y (4) explicar por qué el análisis sintáctico de la entrada es una frontera de seguridad. Se apoya en los capítulos 3 y 7 y prepara el capítulo 9 (datos) y el capítulo 27 (verificación).

8.1 La entrada es una frontera de seguridad

Todo programa que lee datos del exterior (un archivo, un mensaje de red, lo que introduce un operador) hace suposiciones sobre ellos. Cuando las suposiciones son erróneas, los programas se bloquean, calculan mal o, peor aún, hacen algo que eligió un atacante. Los catálogos de debilidades del software están dominados por errores en el tratamiento de la entrada: inyección, lecturas y escrituras fuera de límites, validación incorrecta de la entrada (CWE Top 25). El patrón defensivo es analizar, no adivinar: definir con exactitud cómo es una entrada válida, convertir el texto bruto en valores tipados en un único lugar y rechazar todo lo demás con un error claro.

8.2 Reglas de validación

Para un punto de consigna enviado a una estación de bombeo, una entrada válida podría ser: un número; finito (no «NaN» ni infinito); dentro de los límites de ingeniería (0,5–4,5 m); que no cambie más de 0,5 m respecto del valor actual; procedente de una fuente autorizada. Cada regla debe ser explícita, estar probada y registrarse cuando rechaza algo. La lista de permitidos (aceptar solo las formas conocidas como buenas) es más sólida que la lista de denegados (rechazar las formas conocidas como malas), porque los atacantes encuentran formas en las que usted no pensó. En Python, float("nan") funciona, y tanto nan < 0.5 como nan > 4.5 son falsos, de modo que una comprobación de rango escrita como «rechazar si está por debajo o por encima» acepta NaN sin hacer ruido. La comprobación robusta es «aceptar solo si math.isfinite(x) y 0.5 <= x <= 4.5».

8.3 Errores y fallar de forma segura

Cuando la validación falla, el programa debe hacer algo definido: rechazar la orden y conservar el último valor bueno, generar una alarma, registrar el motivo. No debe continuar con un valor predeterminado que resulte peligroso (un punto de consigna de cero) ni tragarse el error en silencio (except: pass). En contextos de control, «seguro» es una decisión del proceso (una válvula puede necesitar fallar cerrada para el cloro pero abierta para el agua de refrigeración), por lo que el programador implementa un estado de falla que ha especificado el responsable del proceso, en lugar de inventarlo.

8.4 Pruebas

Una prueba unitaria comprueba una función con casos conocidos. Los buenos conjuntos de pruebas incluyen valores típicos, límites (exactamente 0,5 y 4,5), valores justo fuera de los límites (0,49, 4,51), entradas mal formadas (cadena vacía, "abc", "1e309", "nan") y casos dependientes del estado (un valor válido que cambia demasiado rápido). El módulo unittest incorporado en Python o simples sentencias assert bastan en este nivel. Una prueba que nunca ha fallado nunca ha demostrado que funcione: ejecute cada prueba nueva una vez contra una versión estropeada a propósito.

Para el validador de puntos de consigna de CW, la versión contra la que fallar es la v1: su prueba de "nan" devuelve True y su prueba de "abc" lanza una excepción, de modo que dos de las siete pruebas fallan antes de la corrección y ninguna después.

8.5 Ejemplo resuelto: validador de puntos de consigna de CW (sintético)

Este caso es sintético. La interfaz de operador de Calder Water envía como texto los puntos de consigna del nivel de los tanques. Versión 1 del validador:

def accept_v1(text, current):
    x = float(text)
    if x < 0.5 or x > 4.5: return False
    if abs(x - current) > 0.5: return False
    return True

accept_v1("nan", 3.0) devuelve True: ambas comparaciones con NaN son falsas, y abs(nan - 3.0) > 0.5 también es falsa. accept_v1("abc", 3.0) lanza una excepción que, según quién llame, puede bloquear la interfaz. Versión 2:

import math
def accept_v2(text, current):
    try:
        x = float(text)
    except ValueError:
        return False
    return math.isfinite(x) and 0.5 <= x <= 4.5 and abs(x - current) <= 0.5

La versión 2 dice qué es aceptable en lugar de qué no lo es. Pruebas: ("3.2", 3.0) → True; ("4.5", 4.2) → True; ("4.51", 4.2) → False; ("nan", 3.0) → False; ("inf", 3.0) → False; ("abc", 3.0) → False; ("2.0", 3.0) → False (paso demasiado grande).

El bloque try convierte un bloqueo en un rechazo, y math.isfinite elimina NaN e infinito antes de cualquier comparación. Lo que la v2 todavía no hace es decir por qué rechazó: la interfaz de operador debería registrar qué regla falló.

8.6 Actividad práctica

Implemente ambas versiones y las siete pruebas anteriores. Ejecute las pruebas contra la v1 y anote cuáles fallan; después, contra la v2. Añada dos pruebas propias que crea que la v2 todavía podría resolver mal (pista: espacios en blanco, cadenas muy largas, una coma como separador decimal) y decida si el comportamiento es aceptable. Contra la v1, espere que la prueba de "nan" falle (devuelve True) y que "abc" lance una excepción en lugar de devolver False. Contra la v2 deberían pasar las siete. En sus propias pruebas, float de Python acepta " 3.2 " con espacios, lo que probablemente está bien; "3,2" se rechaza, lo que puede sorprender a un operador que use la coma decimal. Decida si ese rechazo debería producir un mensaje claro en lugar de una negativa silenciosa.

8.7 Errores comunes y límites

En CW el tercer error es el peligroso: si solo la interfaz de operador comprueba los puntos de consigna, cualquier otro cliente que pueda llegar al PLC por Modbus evita por completo la validación, como muestra el conducto de la zona de campo del Lab 4. El quinto error enlaza con el Lab 8: un punto de consigna bien formado de 2,0 m pasa la v2 tanto si lo envió un operador como si lo envió un intruso.

Ejercicios — capítulo 8

EX015 — Romper la v1. Objetivo: encontrar entradas que burlen una comprobación por lista de denegados. Entradas: accept_v1. Pasos: encuentre tres entradas distintas para las que la v1 da un resultado inseguro o indefinido y explique cada una. Evidencia esperada: las entradas y las explicaciones. Rúbrica: 3 = NaN aceptado; texto no numérico (o una cadena vacía) que lanza una excepción no gestionada; y un tercer caso distinto, como un valor actual NaN, que hace que la comprobación del paso se supere para cualquier punto de consigna dentro de rango; 2 = dos; 1 = una; 0 = nada.

EX016 — Especificar el estado de falla. Objetivo: conectar el código con la seguridad funcional del proceso. Entradas: un punto de consigna rechazado en una bomba de dosificación de cloro y en una válvula de entrada de agua bruta. Pasos: para cada caso, indique qué debe hacer el programa ante el rechazo, quién debe decidirlo y qué debe registrarse. Evidencia esperada: dos especificaciones breves. Rúbrica: 3 = conservar el último valor bueno más alarma, decide el responsable del proceso, campos del registro nombrados (hora, fuente, entrada bruta, regla que falló); 2 = parcial; 1 = genérico; 0 = nada.

Síntesis del capítulo

El análisis de la entrada es una frontera de seguridad. Defina con precisión la entrada válida, acepte solo esa, falle hacia un estado elegido por el responsable del proceso, registre cada rechazo y pruebe los límites y los casos mal formados. La validación detecta la entrada mal formada; no demuestra que una entrada bien formada procediera de alguien autorizado.

Respuestas desarrolladas

EX015. (1) "nan" se acepta porque toda comparación con NaN es falsa. (2) "abc" (o "") lanza ValueError, lo que bloquea a quien llama o lo deja en un estado indefinido. (3) Si current es NaN (por ejemplo, por una lectura fallida del sensor), abs(x - current) > 0.5 es falsa para cualquier x, de modo que cualquier punto de consigna dentro de rango supera la comprobación del paso. "inf" se rechaza en la comprobación de rango, pero solo porque es mayor que 4,5, lo que es suerte, no diseño.

EX016. Bomba de dosificación de cloro: conservar el último punto de consigna bueno, generar una alarma para el operador y no moverse hacia el máximo; el responsable del proceso (el responsable de calidad del agua) decide este comportamiento ante la falla. Válvula de entrada de agua bruta: conservar la última posición buena y generar una alarma; operaciones decide si se necesita una posición de respaldo para proteger la capacidad de tratamiento. Registro en ambos casos: marca de tiempo, fuente (usuario o estación), entrada bruta, valor analizado si lo hay, regla que falló, valor actual y acción realizada.