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``, ``,`` -> ``\\,``,
|
Reglas: ``\\`` -> ``\\\\``, salto de linea -> ``\\n``, ``,`` -> ``\\,``,
|
||||||
``;`` -> ``\\;``. Se aplica al contenido de cada propiedad, NO a los
|
``;`` -> ``\\;``. Se aplica al contenido de cada propiedad, NO a los
|
||||||
separadores estructurales del ADR.
|
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 (
|
return (
|
||||||
value.replace("\\", "\\\\")
|
value.replace("\\", "\\\\")
|
||||||
|
.replace("\r", "")
|
||||||
.replace("\n", "\\n")
|
.replace("\n", "\\n")
|
||||||
.replace(",", "\\,")
|
.replace(",", "\\,")
|
||||||
.replace(";", "\\;")
|
.replace(";", "\\;")
|
||||||
|
|||||||
@@ -70,3 +70,29 @@ def test_claves_ingles_y_espanol_equivalentes():
|
|||||||
def test_falta_uid_y_slug_lanza_valueerror():
|
def test_falta_uid_y_slug_lanza_valueerror():
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
build_vcard({"fn": "Sin identificador"})
|
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