Back to flin
flin

Funciones de validación y saneamiento

Cómo FLIN incluye 67 funciones de validación y saneamiento como integradas -- desde verificaciones de email y URL hasta saneamiento de HTML y prevención de inyección SQL, todo sin importar una biblioteca.

Juste A. Gnimavo (Thales) & Claude | March 26, 2026 11 min flin
EN/ FR/ ES
flinrust

Every web application has the same vulnerability: user input. A form field that expects an email address receives JavaScript injection. A search box that expects text receives an SQL query. A comment field that expects words receives a <script> tag. The only defense is validation (rejecting invalid input) and sanitization (neutralizing dangerous input).

In most languages, this defense requires installing packages. validator for JavaScript. django.core.validators for Python. govalidator for Go. Each package has its own API, its own edge cases, and its own vulnerabilities. In FLIN, validation and sanitization are built into the language. Sessions 131 and 190 through 192 added 67 functions that cover every validation and sanitization need a web application has.

Las dos líneas de defensa

FLIN separates validation (checking whether input is valid) from sanitization (making input safe). Both are necessary. Validation tells you whether to accept input. Sanitization ensures that accepted input cannot cause harm.

flin// Validation: is this input acceptable?
email = "[email protected]"
email.is_email              // true -- accept it

// Sanitization: make this input safe for display
comment = "<script>alert('xss')</script>Hello!"
safe = sanitize_html(comment)
// "Hello!" -- the script tag is removed

Validation rejects bad input at the boundary. Sanitization neutralizes input that passes validation but might still be dangerous in a specific context (HTML display, SQL queries, URL construction).

Funciones de validación

Validación de formato de texto

flintext.is_email              // RFC 5322 simplified email check
text.is_url                // Valid URL with protocol
text.is_ip                 // IPv4 or IPv6 address
text.is_ipv4               // IPv4 address only
text.is_ipv6               // IPv6 address only
text.is_uuid               // UUID v4 format
text.is_json               // Valid JSON string
text.is_hex                // Hexadecimal characters only
text.is_base64             // Valid base64 string

Each validation function returns a boolean. No exceptions. No error objects. Just true or false. If you need to know why validation failed (for user-facing error messages), the entity constraint system provides that:

flinentity User {
    email: text where is_email  // Constraint with automatic error message
    age: int where >= 13 and <= 120
    username: text where len >= 3 and len <= 30
}

// When validation fails, you get a specific error:
result = User.create(email: "not-an-email", age: 10)
// Error: "email: must be a valid email address"
// Error: "age: must be >= 13"

Validación numérica

flintext.is_numeric            // All digits (0-9)
text.is_alpha              // All letters (a-z, A-Z)
text.is_alphanumeric       // Letters and digits only
text.is_integer            // Valid integer string ("-42", "0", "100")
text.is_float              // Valid float string ("3.14", "-0.5")

These functions validate string content without converting it. "42".is_numeric returns true without producing an integer value. This is the correct approach for form validation: first check that the input is valid, then convert it.

Validación de patrones

flintext.matches(pattern)       // Regex match
text.is_phone              // International phone number format
text.is_credit_card        // Credit card number (Luhn check)
text.is_hex_color          // "#ff6b35" or "#f63"
text.is_slug               // URL slug (lowercase, hyphens, no spaces)
text.is_semver             // Semantic version ("1.2.3")

is_phone validates international phone numbers with country codes. It does not validate that the number exists -- only that its format is plausible. This is sufficient for most web applications, where phone verification happens via SMS.

is_credit_card performs a Luhn checksum validation. It does not validate that the card is active or has funds -- that requires a payment processor API call. But the Luhn check catches typos and random numbers, preventing unnecessary API calls.

Validación de longitud y rango

flintext.len_between(3, 50)    // Length between 3 and 50 characters
text.len_min(3)            // At least 3 characters
text.len_max(50)           // At most 50 characters
n.between(1, 100)          // Number in range [1, 100]
n.positive                 // Greater than 0
n.negative                 // Less than 0

These are convenience wrappers around comparisons. text.len_between(3, 50) is equivalent to text.len >= 3 and text.len <= 50, but it reads better in validation chains and entity constraints.

Funciones de saneamiento

Saneamiento HTML

HTML sanitization is the primary defense against Cross-Site Scripting (XSS) attacks. FLIN provides three levels:

flin// Level 1: Escape HTML entities (preserve all text, neutralize HTML)
html_escape("<script>alert('xss')</script>")
// "&lt;script&gt;alert('xss')&lt;/script&gt;"

// Level 2: Strip all HTML tags (keep text content only)
strip_tags("<b>Hello</b> <script>evil()</script> World")
// "Hello  World"

// Level 3: Sanitize HTML (keep safe tags, remove dangerous ones)
sanitize_html("<b>Bold</b> <script>evil()</script> <a href='url'>Link</a>")
// "<b>Bold</b>  <a href='url'>Link</a>"

html_escape converts every <, >, &, ", and ' to their HTML entity equivalents. This is the safest approach -- it preserves the input text but ensures nothing is interpreted as HTML.

strip_tags removes all HTML tags and returns only the text content. This is useful when you need plain text from user input that might contain HTML (pasted from a rich text editor, for example).

sanitize_html is the most sophisticated option. It maintains a whitelist of safe tags (<b>, <i>, <a>, <p>, <br>, <ul>, <li>, <h1> through <h6>, etc.) and removes everything else. Attributes are also filtered -- href is allowed on <a> tags but onclick is stripped. style attributes are removed entirely to prevent CSS injection.

flin// Sanitize user-generated content for display
entity Comment {
    author: text
    body: text
    sanitized_body: text
}

fn create_comment(author: text, raw_body: text) {
    Comment.create(
        author: author,
        body: raw_body,                    // Store original
        sanitized_body: sanitize_html(raw_body)  // Store sanitized
    )
}

Prevención de inyección SQL

FLIN's entity system uses parameterized queries internally, which prevents SQL injection by design. But for developers who construct raw queries (escape hatch for advanced use cases), sanitization functions are available:

flin// Escape a string for safe SQL inclusion
safe = escape_sql("Robert'; DROP TABLE users; --")
// "Robert''; DROP TABLE users; --"

// Escape for LIKE patterns
safe_pattern = escape_sql_like("100%")
// "100\%"

escape_sql doubles single quotes, which prevents the classic SQL injection attack. escape_sql_like also escapes % and _ wildcards, preventing pattern injection in LIKE queries.

In practice, FLIN developers should almost never need these functions. The entity system's query API is parameterized:

flin// Safe by default -- parameterized query
user = User.find_by(email: user_input)

// Also safe -- where clause is parameterized
users = User.where(name: search_term)

Saneamiento de URLs

flin// Encode for URL parameters
url_encode("hello world & more")
// "hello%20world%20%26%20more"

// Decode URL parameters
url_decode("hello%20world")
// "hello world"

// Build a safe URL
base = "https://api.example.com/search"
query = url_encode(user_input)
safe_url = "{base}?q={query}"

URL encoding ensures that special characters in user input do not break URL structure or get interpreted as URL components.

Prevención de travesía de rutas

flin// Sanitize a file path
safe_path = sanitize_path("../../etc/passwd")
// "etc/passwd" (relative traversal removed)

// Validate that a path is within a base directory
is_safe = path_within("/uploads", user_provided_path)

sanitize_path removes .. components and leading slashes, preventing directory traversal attacks. path_within checks that a resolved path stays within a specified base directory.

Validación a nivel de entidad

FLIN's entity system integrates validation at the schema level using where constraints:

flinentity Product {
    name: text where len >= 1 and len <= 200
    description: text where len <= 5000
    price: float where > 0
    sku: text where matches("^[A-Z]{2}-\\d{4}$")
    email: text where is_email
    url: text? where is_url
    category: text where in ["electronics", "clothing", "food", "other"]
}

These constraints are checked automatically on create and update operations. If any constraint fails, the operation returns an error with a descriptive message. The developer does not need to write validation logic separately -- it is declared once in the entity definition and enforced everywhere.

flin// This fails validation automatically
result = Product.create(
    name: "",                    // Too short
    price: -5.0,                 // Not positive
    sku: "invalid",              // Does not match pattern
    email: "not-an-email"        // Not a valid email
)
// Errors:
// - "name: length must be >= 1"
// - "price: must be > 0"
// - "sku: must match pattern ^[A-Z]{2}-\d{4}$"
// - "email: must be a valid email address"

Componer validación y saneamiento

Real-world input handling combines validation and sanitization in sequence:

flinfn process_user_input(raw_name: text, raw_email: text, raw_bio: text) {
    // Step 1: Sanitize (make safe)
    name = raw_name.trim
    email = raw_email.trim.lower
    bio = sanitize_html(raw_bio)

    // Step 2: Validate (check acceptability)
    errors = []

    {if name.is_empty}
        errors.push("Name is required")
    {else if name.len > 100}
        errors.push("Name must be 100 characters or less")
    {/if}

    {if not email.is_email}
        errors.push("A valid email address is required")
    {/if}

    {if bio.len > 5000}
        errors.push("Bio must be 5000 characters or less")
    {/if}

    {if errors.len > 0}
        return { success: false, errors: errors }
    {/if}

    // Step 3: Persist (store validated, sanitized data)
    user = User.create(name: name, email: email, bio: bio)
    return { success: true, user: user }
}

The pattern is always the same: sanitize first (trim, normalize, remove dangerous content), validate second (check format and constraints), persist third (store the clean data). FLIN's built-in functions make each step a single line.

Implementación: validadores Rust compilados

Each validation function is implemented as a Rust function that operates directly on the string's bytes. No regular expressions are used for standard validations (email, URL, IP address). Instead, each validator is a hand-written state machine that is both faster and more correct than a regex.

rustfn is_valid_email(s: &str) -> bool {
    // Simplified RFC 5322 validation
    let at_pos = match s.find('@') {
        Some(pos) if pos > 0 => pos,
        _ => return false,
    };

    let local = &s[..at_pos];
    let domain = &s[at_pos + 1..];

    // Local part validation
    if local.is_empty() || local.len() > 64 {
        return false;
    }

    // Domain validation
    if domain.is_empty() || domain.len() > 253 {
        return false;
    }

    // Domain must contain a dot
    if !domain.contains('.') {
        return false;
    }

    // TLD must be at least 2 characters
    let tld = domain.rsplit('.').next().unwrap();
    if tld.len() < 2 {
        return false;
    }

    true
}

This email validator is not a complete RFC 5322 implementation (which would allow quoted strings, comments, and other rarely-used features). It is a practical validator that accepts every email address a real user would have and rejects obvious non-emails. This pragmatic approach matches what every production email validator does -- including the ones in validator.js, django.core.validators, and govalidator.

The HTML sanitizer uses a streaming parser that processes the input character by character, maintaining a stack of open tags and a whitelist of allowed tags and attributes. It never builds a DOM tree in memory, so it handles arbitrarily large inputs without memory pressure.

Lo que validamos vs. lo que saneamos

The distinction between validation and sanitization is often confused. Here is FLIN's clear separation:

Validation functions answer the question "is this input acceptable?" They return booleans and do not modify the input:

  • is_email, is_url, is_ip, is_uuid
  • is_numeric, is_alpha, is_alphanumeric
  • is_phone, is_credit_card, is_hex_color
  • len_between, between, matches

Sanitization functions answer the question "how do I make this input safe?" They return modified strings:

  • html_escape, strip_tags, sanitize_html
  • escape_sql, escape_sql_like
  • url_encode, url_decode
  • sanitize_path
  • trim, lower, upper (normalization)

Using both together is the correct pattern: sanitize input before storage, validate input before processing, sanitize output before display. FLIN makes all three steps trivial.

Sesenta y siete funciones, cero vulnerabilidades

The complete validation and sanitization API:

  • 15 format validators (email, URL, IP, UUID, phone, credit card, etc.)
  • 5 content validators (numeric, alpha, alphanumeric, empty, blank)
  • 6 length/range validators (len_between, len_min, len_max, between, etc.)
  • 1 pattern validator (matches)
  • 3 HTML sanitizers (html_escape, strip_tags, sanitize_html)
  • 1 HTML unescaper (html_unescape)
  • 2 SQL sanitizers (escape_sql, escape_sql_like)
  • 2 URL encoders (url_encode, url_decode)
  • 2 path sanitizers (sanitize_path, path_within)
  • 2 base64 coders (base64_encode, base64_decode)
  • 28 entity constraint keywords (where, in, between, matches, etc.)

Sixty-seven functions that replace validator, sanitize-html, xss, dompurify, express-validator, and a dozen other security-related packages. All built-in. All handling edge cases correctly. All making the secure path the default path.


Esta es la Parte 79 de la serie "Cómo construimos FLIN", que documenta cómo un CEO en Abiyán y un CTO de IA integraron validación de entrada y saneamiento directamente en un lenguaje de programación.

Navegación de la serie: - [78] Reduce, Map, Filter: funciones de orden superior - [79] Funciones de validación y saneamiento (estás aquí) - [80] Seguimiento de errores y monitoreo de rendimiento - [81] FlinUI: sistema de componentes sin importaciones

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles

Thales & Claude deblo

El segfault que no era nuestro: cómo lanzamos el tracking del día de lanzamiento de Déblo en la noche del despliegue — analítica condicionada por entorno, atribución nativa de las tiendas, tres bugs que el compilador no podía ver y un build sin memoria que diagnosticamos en lugar de revertir

El 1 de julio de 2026 — el día del lanzamiento — el riesgo nunca fue el texto. Era que las campañas de pago salieran a ciegas. Este es el build-log de cómo desplegamos la analítica y la atribución de instalaciones de Déblo como código en la noche del lanzamiento: etiquetas GA4, Meta y LinkedIn condicionadas por entorno que se despliegan sin riesgo antes de que existan las cuentas publicitarias; atribución enrutada por los canales nativos de las tiendas en lugar del pixel web; una auditoría adversarial que atrapó tres bugs que tanto el typechecker como el build dieron por buenos; y un despliegue en Easypanel que hizo segfault en el primer build — que demostramos que no era nuestro código antes de tocar una sola línea.

18 min Jul 1, 2026
deblolaunch-dayclaude-opus-4.8claude-code +26
Thales & Claude thales

Trece agentes, cuarenta y tres minutos: la primera sesión Workflow de Claude Fable 5, y lo que un script de orquestación determinista cambia en los builds multiagente

Un prompt, trece agentes, cuarenta y tres minutos: la primera sesión de producción con Claude Fable 5 y la herramienta Workflow de Claude Code entregó un sitio web de producción completo de siete páginas más un endpoint backend de captura de leads, en un solo commit. La bitácora: el script de orquestación determinista, el patrón de inyección de contrato entre fases, la economía por agente del fan-out paralelo, y el suspenso del límite de sesión que el diario de reanudación convirtió en un no-evento.

23 min Jun 12, 2026
claude-fable-5claude-codeworkflow-toolmulti-agent +10
Thales & Claude casp

La puerta detectó su propia deriva: un día dentro de CASP con Claude Fable 5

Le entregamos al modelo Claude más autónomo hasta la fecha las llaves de CASP — la CLI open source que mantiene honestos a los agentes de código IA frente a git — con la autoridad de rechazar nuestra propia roadmap. Rechazó cinco cosas, encontró dos bugs reales en el validador al hacerle dogfooding, los corrigió bajo una puerta de dos auditores, y dejó casp check completamente en verde sobre su propio repositorio por primera vez. CASP 0.3.0 es el resultado.

15 min Jun 10, 2026
caspzerosuiteworkflowai-cto +9