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 <noreply@anthropic.com>
This commit is contained in:
@@ -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(";", "\\;")
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user