From b8ec97e47734fd91c04c21bfb0770d94a29cf574 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sat, 13 Jun 2026 11:19:43 +0200 Subject: [PATCH] fix(security): build_vcard neutraliza el retorno de carro crudo (anti CR-injection vCard) El escape de valores vCard solo escapaba el salto de linea, no el retorno de carro crudo. Un \r sin \n sobrevivia al escape y los parsers que lo normalizan a salto de linea (como _unfold_lines de osint_web) leian propiedades inyectadas (p.ej. X-OSINT-DNI), burlando el control de no exponer datos OSINT al movil. Ahora _vcard_escape elimina el \r, en paridad con el escape iCal. Test de regresion anadido. Co-Authored-By: Claude Fable 5 --- python/functions/core/build_vcard.py | 8 +++++++ python/functions/core/build_vcard_test.py | 26 +++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/python/functions/core/build_vcard.py b/python/functions/core/build_vcard.py index 1dd3a265..7ea8fe55 100644 --- a/python/functions/core/build_vcard.py +++ b/python/functions/core/build_vcard.py @@ -23,9 +23,17 @@ def _vcard_escape(value: str) -> str: Reglas: ``\\`` -> ``\\\\``, salto de linea -> ``\\n``, ``,`` -> ``\\,``, ``;`` -> ``\\;``. Se aplica al contenido de cada propiedad, NO a los separadores estructurales del ADR. + + El retorno de carro ``\\r`` crudo se ELIMINA (no se escapa): un ``\\r`` solo, + sin ``\\n`` que lo siga, sobrevive al escape de ``\\n`` y queda como carácter de + control dentro del valor. Varios parsers de vCard (y el propio ``_unfold_lines`` + de osint_web, que normaliza ``\\r`` a ``\\n``) lo tratan como un separador de + línea, lo que permitiría inyectar propiedades nuevas (p. ej. ``X-OSINT-DNI``) + en la tarjeta. Eliminarlo cierra ese vector, en paridad con el escape iCal. """ return ( value.replace("\\", "\\\\") + .replace("\r", "") .replace("\n", "\\n") .replace(",", "\\,") .replace(";", "\\;") diff --git a/python/functions/core/build_vcard_test.py b/python/functions/core/build_vcard_test.py index bea72107..d263a7bc 100644 --- a/python/functions/core/build_vcard_test.py +++ b/python/functions/core/build_vcard_test.py @@ -70,3 +70,29 @@ def test_claves_ingles_y_espanol_equivalentes(): def test_falta_uid_y_slug_lanza_valueerror(): with pytest.raises(ValueError): build_vcard({"fn": "Sin identificador"}) + + +def test_cr_crudo_no_inyecta_propiedades(): + """Un '\\r' crudo en un valor no debe poder inyectar una propiedad nueva. + + Sin neutralizar el '\\r', un parser que normalice '\\r' a salto de línea (como + el _unfold_lines de osint_web) leería 'X-OSINT-DNI' / 'X-EVIL' como propiedades + legítimas, burlando el control de "no exponer X-OSINT-* al móvil". El escape + debe eliminar el '\\r' para que el valor quede en una sola línea física. + """ + vcard = build_vcard( + { + "uid": "victima", + "fn": "Bob\rX-OSINT-DNI:11111111H\rX-EVIL:pwned", + "tels": ["911\rNOTE:leak"], + } + ) + # Simula el unfold de osint_web: '\r\n' y '\r' sueltos pasan a salto de línea. + physical_lines = vcard.replace("\r\n", "\n").replace("\r", "\n").split("\n") + inyectadas = [ + ln for ln in physical_lines if ln.startswith(("X-OSINT-DNI", "X-EVIL", "NOTE")) + ] + assert inyectadas == [], f"propiedades inyectadas via CR: {inyectadas}" + # El '\r' no debe sobrevivir en el texto serializado salvo como CRLF de línea. + assert "\rX-OSINT" not in vcard + assert "\rNOTE" not in vcard