Relatório Técnico Confidencial

Segurança e Arquitetura
AeroMais — Sistema de Medição

Documentação consolidada de todas as pendências de segurança levantadas ao longo do projeto, como foram resolvidas, e demonstração de que os objetivos de segurança foram atingidos para avaliação de pentest.

Data Abril / 2026
Versão 1.1 — Relay dual-socket
Componentes Server · App · Display
Stack Flask · Flutter · Nginx · WireGuard
PTRZNs Documentados 16 itens
Classificação Confidencial / Interno

1. Resumo Executivo

O AeroMais é um sistema embarcado de medição de combustível composto por: um servidor Python/Flask rodando em Raspberry Pi, um aplicativo Android (Flutter) com WebView, um display embarcado e integração com a plataforma Automais.IO via WireGuard VPN.

Durante o processo de desenvolvimento, foram identificadas e tratadas 16 pendências de segurança (referenciadas como PTRZN-XXXX). Cada pendência foi documentada, analisada e resolvida com código rastreável nos comentários do fonte.

Este relatório apresenta: (1) a arquitetura do sistema, (2) os fluxos de autenticação e WiFi, (3) o detalhamento de cada PTRZN com o problema original e a solução implementada, e (4) a demonstração de que os objetivos de segurança máxima foram atingidos.

12 Resolvidos
1 Em Desenvolvimento
1 Pendente
2 Features / Melhorias
PTRZNTítuloSeveridadeStatus
1516Disclosure de tecnologias via headersMédiaResolvido
1517Detecção de root (Android)AltaResolvido
1518Detecção de emulador (Android)MédiaResolvido
1520Integridade do APK (anti-tamper)AltaResolvido
1521Ausência de headers de segurança HTTPMédiaResolvido
1522Exposição de stack traces em erros HTTPMédiaResolvido
1523Cookie de sessão sem flags Secure/HttpOnlyAltaResolvido
1525Hash de senha MD5 → bcryptAltaResolvido
1526Segredos hardcoded → variáveis de ambienteAltaResolvido
1527Política de senha fracaMédiaResolvido
1528IDOR — exposição de IDs internosAltaEm Dev
1529Usuário duplicado / lógica de criaçãoBaixaResolvido
1530Sessão não invalidada no logoutAltaResolvido
1531XSS via innerHTML e campos sem restriçãoAltaResolvido
1532Validação de entrada insuficienteMédiaResolvido
1538Ausência de rate limiting no loginAltaResolvido
1540Remote View — captura de tela dualFeature

3. Contexto Técnico e Progresso

Status atual — Abril 2026
Todos os itens de segurança levantados foram endereçados e validados item por item, verificando requests e responses. Este manual documenta o estado atual como era e como está, servindo de referência tanto para a equipe de pentest quanto para a direção da Raízen.

3.1 Principal Ponto de Atenção — Enlace Tablet ↔ CPMais

O maior desafio contínuo do projeto é garantir que a comunicação entre o tablet Android (AeromaisApp) e o CPMais (dispositivo Linux embarcado) seja estabelecida sobre HTTPS com certificados seguros — e que essa comunicação se mantenha estável ao longo do tempo, independente das condições de campo.

3.2 Decisão Arquitetural — CPMais como Hotspot (AP)

Durante os testes observamos um comportamento de instabilidade quando o CPMais atuava como cliente WiFi (STA) e o tablet como ponto de acesso: o tablet por vezes derrubava o roteador interno, e o CPMais não se reconectava de forma confiável. O caminho inverso se mostrou muito mais estável.

CenárioTabletCPMaisResultado
Anterior Hotspot (AP) Cliente WiFi (STA) ❌ Instável — tablet derrubava o AP, CPMais não reconectava
Atual Cliente WiFi (STA) Hotspot (AP) ✅ Estável — Android excelente em manter e reconectar ao AP conhecido; Linux excelente em manter o AP em pé
Por que essa combinação funciona melhor
O Android é otimizado para encontrar e reconectar automaticamente a redes WiFi conhecidas — comportamento nativo do sistema. O Linux (CPMais) é robusto em manter um AP ativo sem interrupções. Usar cada dispositivo no papel em que é naturalmente bom resultou em uma resiliência de enlace significativamente superior.

3.3 Desafio Derivado — CPMais sem Acesso à Internet

Com o CPMais em modo AP, ele deixa de ser um cliente de rede — portanto não tem acesso direto à internet. Isso levantou um problema: como fazer manutenção remota e sincronizar certificados sem expô-los?

Solução: Relay UDP via AeromaisApp

O app Android atua como relay de pacotes UDP da VPN do CPMais para o servidor VPN da plataforma.

Desafio de Roteamento Android — WiFi vs. 4G

Um obstáculo crítico surgiu durante os testes: ao conectar ao AP do CPMais, o Android detecta que o WiFi não tem internet e exibe o diálogo "rede sem internet, conectar mesmo assim?". Em vários dispositivos/OEMs, ao confirmar, o Android passa a rotear todo o tráfego pelo WiFi — incluindo os pacotes do relay UDP. Como o CPMais não tem saída para a internet, esses pacotes eram descartados no RPi e o relay nunca conseguia completar o handshake WireGuard.

Um segundo problema era o DNS: o dnsmasq do AP está configurado com address=/#/10.254.254.1, fazendo todos os domínios resolverem para o IP do CPMais. Com isso, automais.io também resolvia para 10.254.254.1, e o relay tratava os pacotes do CPMais como respostas do servidor remoto, descartando-os silenciosamente.

Solução: Dual-Socket + DNS Celular + Captive Portal 302

Três correções complementares foram implementadas:

MecanismoArquivoEfeito
Captive portal 302 wifimaismanager.py Responde 302 (redirect) em vez de 204 ao probe do Android. Com 302, o Android marca a rede como "captive portal" e mantém o 4G como rota padrão — todos os outros apps e o sistema operacional continuam usando celular sem interrupção.
Dual-socket no relay udp_relay_service.dart _socketLocal vinculado ao IP WiFi AP (10.254.254.x:51821) recebe pacotes WireGuard do CPMais. _socketRemote vinculado ao IP celular (porta efêmera) envia e recebe de automais.io:51820. O tráfego para o servidor sai obrigatoriamente pela interface celular, independente do roteamento do Android.
DNS via rede celular RouterMonitorPlugin.java Novo método getCellularInfo() usa ConnectivityManager para encontrar a rede celular e chama Network.getAllByName("automais.io") — resolve o DNS diretamente pelo DNS do operador, ignorando o dnsmasq do AP.

O fluxo completo do relay com dual-socket:

sequenceDiagram
    participant C as CPMais (AP · 10.254.254.1)
    participant WL as _socketLocal (WiFi 10.254.254.x:51821)
    participant WR as _socketRemote (IP Celular:efêmero)
    participant V as automais.io:51820

    Note over C,WL: Enlace local WiFi AP (sem internet)
    Note over WR,V: Enlace celular 4G (internet)

    C->>WL: Handshake WireGuard UDP
    WL->>WR: relay (memória interna)
    WR->>V: Reencaminha via 4G
    V-->>WR: Resposta WireGuard
    WR->>WL: relay
    WL-->>C: Entrega ao CPMais

    Note over C,V: Túnel WireGuard estabelecido via relay
    C->>V: Tráfego de manutenção / PKI (via túnel)
    V-->>C: Configuração remota / PKI enrollment
      

3.4 Resultado Final — HTTPS Seguro sem Exposição de Certificados

Com o túnel VPN viabilizado pelo relay do AeromaisApp, o CPMais consegue:

  • Relay UDP estável — dual-socket — o relay usa dois sockets separados: um vinculado ao IP WiFi AP (para o CPMais) e outro ao IP celular (para automais.io). Isso garante que o tráfego de relay saia obrigatoriamente pelo 4G, independente de como o Android roteou o tráfego ao conectar no AP. O DNS de automais.io é resolvido via ConnectivityManager da rede celular, contornando o dnsmasq do AP (que retornaria 10.254.254.1 para todos os domínios).
  • 4G mantido para outros apps e sistema — o captive portal do AP responde com 302 (redirect) em vez de 204 ao probe /generate_204 do Android. Com 204, o Android consideraria o WiFi como "rede com internet" e rotearia tudo pelo CPMais. Com 302, o Android marca a rede como "captive portal" e mantém o 4G como rota padrão de internet para todos os apps e o sistema operacional.
  • Enrollment de certificados via PKI Service — o CPMais gera o par de chaves localmente, envia apenas o CSR (nunca a chave privada) e recebe o certificado assinado pela plataforma, tudo pelo canal VPN.
  • Manutenção e configuração remota — acesso seguro ao CPMais via túnel VPN sem expor nenhuma porta diretamente à internet.
  • HTTPS entre tablet e CPMais — após o enrollment, toda a comunicação entre o AeromaisApp e o CPMais ocorre sobre HTTPS/mTLS com certificados emitidos pela CA privada da plataforma. Nenhum certificado é transferido manualmente ou exposto durante o processo.
  • Renovação automática — o CPMais verifica diariamente se o certificado precisa ser renovado e executa o processo automaticamente via o mesmo canal VPN, sem intervenção humana.
Síntese para o pentest e para a direção
A arquitetura de enlace atual elimina os principais vetores de ataque na camada de comunicação local: não há troca manual de certificados, não há portas abertas na internet, não há WiFi com senha estática — toda a segurança é derivada de criptografia de chave pública com emissão dinâmica e de um túnel VPN gerenciado pela própria plataforma. O esforço contínuo de estabilidade e segurança nesse enlace é o reflexo direto do compromisso com uma entrega inquestionável, profissional e segura.

2. Topologia do Sistema

O AeroMais é composto por quatro camadas físicas que se comunicam de forma segura. A segurança não reside em um único componente mas na composição das camadas.

graph TB
    subgraph cloud["☁️  Plataforma Automais.IO"]
        WGS["WireGuard\nVPN Server"]
        PKI["PKI Service"]
        KV["🔐 Automais.IO\nCA RSA-4096\n(chave gerenciada internamente)"]
        PG[("Audit log\ncertificados e tokens")]
        KV-->|Sign| PKI
        PKI---PG
        WGS---PKI
    end

    subgraph rpi["🖥️  Raspberry Pi — AeroMaisServer"]
        NGINX["Nginx\nmTLS · porta 443"]
        FLASK["Flask\n127.0.0.1\n(não exposto)"]
        DB[("MariaDB")]
        WIFI["WiFiMaisManager\nhostapd · dnsmasq · iptables"]
        NGINX-->FLASK
        FLASK---DB
    end

    subgraph app["📱  AeromaisApp — Android"]
        WV["WebView\nInterface Flask"]
        WGC["WireGuard Client\nwg-automais"]
        RM["RouterMonitor\nrelay UDP"]
        WV---WGC
        RM---WGC
    end

    DISPLAY["⚙️  AeromaisDisplay\nC++ · RoxProtocol"]

    WGS-->|"VPN wg-automais"| WGC
    WGS-->|"VPN wg-automais"| NGINX
    app<-->|"WiFi AP · 10.254.254.x"| rpi
    FLASK-->|"Serial UART"| DISPLAY
      

Componentes e responsabilidades

ComponenteStackResponsabilidade
AeromaisServerPython 3 / Flask / Nginx / MariaDBAPI REST, interface web, leitura do medidor, gerenciamento WiFi, PKI enrollment
AeromaisAppFlutter / Dart / Kotlin (Android)Interface do operador, relay WireGuard, monitoramento de rede, captura remota
AeromaisDisplayC++Exibição de dados do medidor via display físico, protocolo RoxProtocol serial
Plataforma Automais.IOPKI Service (emissão de certificados), servidor WireGuard VPN, gerenciamento de tokens de enrollment
Automais.IOArmazenamento seguro da chave privada da CA RSA 4096 — gerenciada internamente pela plataforma

3. Fluxo de Autenticação

A autenticação do AeroMais opera em duas camadas independentes e complementares: a camada TLS mútua (mTLS) resolvida no Nginx antes de qualquer HTTP, e a camada de sessão de aplicação gerenciada pelo Flask.

3.1 Camada 1 — mTLS (Mutual TLS) via Nginx

Toda requisição ao servidor Flask passa obrigatoriamente pelo Nginx. O Nginx está configurado com ssl_verify_client on — qualquer conexão sem certificado de cliente válido assinado pela CA privada é rejeitada na camada TLS, antes de qualquer código Python ser executado.

sequenceDiagram
    autonumber
    participant App as AeromaisApp
    participant N as Nginx (porta 443)
    participant F as Flask (127.0.0.1)

    App->>N: TLS ClientHello
    N-->>App: ServerHello + server.crt
    App->>N: client.crt (mTLS)
    N->>N: Verifica client.crt vs ca.crt ✓
    N->>F: proxy_pass + X-SSL-Client-Cert header
    F-->>N: HTTP Response
    N-->>App: HTTP Response

    Note over App,N: Cenário sem certificado de cliente
    App->>N: TLS ClientHello (sem client.crt)
    N-->>App: 400 No required SSL certificate sent
    Note right of F: Flask nunca é alcançado
      

3.2 Camada 2 — Sessão Flask (para operadores humanos)

A interface web (acessada via WebView do app) usa sessão Flask baseada em cookies assinados. O fluxo completo de login com todas as proteções PTRZN aplicadas:

1
Validação de entrada (PTRZN-1532)

O backend valida tamanho, tipo e padrão dos campos username e password antes de qualquer processamento.

2
Rate limiting (PTRZN-1538)

O RateLimiter verifica se o usuário/IP não excedeu o limite de tentativas. Bloqueia temporariamente após falhas repetidas.

3
Verificação bcrypt (PTRZN-1525)

A senha é verificada com bcrypt.checkpw(). O hash armazenado no banco é sempre bcrypt — MD5 foi removido.

4
Geração de sessão (PTRZN-1530)

Um session_id único (secrets.token_urlsafe(32)) é gerado e registrado no SessionManager em memória.

5
Cookie seguro (PTRZN-1523)

O cookie aeromais_session é emitido com flags Secure, HttpOnly e SameSite=Lax.

6
Verificação em cada requisição (PTRZN-1530)

Todos os endpoints protegidos verificam se o session_id ainda está na lista de sessões ativas (não revogado).

7
Logout seguro (PTRZN-1530)

No logout, a sessão é explicitamente revogada no SessionManager — qualquer cookie remanescente é inválido imediatamente.

3.3 Camada 3 — Proteções do App Android

Antes de qualquer requisição ser enviada ao servidor, o app verifica o ambiente de execução:

VerificaçãoPTRZNAção se detectado
Dispositivo rooteado (5 estratégias)1517Tela de bloqueio não-dispensável, app encerrado
Ambiente emulado (4 estratégias)1518Tela de bloqueio não-dispensável, app encerrado
APK adulterado (assinatura / checksum)1520Tela de bloqueio não-dispensável, app encerrado
mTLS — certificate pinning1515Rejeita qualquer CA que não seja a CA privada do projeto
Por que verificar no app E no servidor?
O servidor não tem visibilidade sobre o hardware do cliente — qualquer header HTTP pode ser forjado com Burp Suite. As verificações no app ocorrem no processo em risco, antes de qualquer código de autenticação ser alcançado. As duas camadas são independentes e complementares: uma não substitui a outra.

4. WiFiMaisManager — Topologia e Fluxo

O WiFiMaisManager é o módulo do AeroMaisServer responsável pelo gerenciamento inteligente da interface WiFi wlan0 do RPi. Ele opera como uma máquina de dois estados persistidos em /var/lib/aeromais/wifi_state.json.

stateDiagram-v2
    direction TB
    [*] --> UNCONFIGURED : Boot · sem wifi_state.json

    state UNCONFIGURED {
        direction TB
        s1 : wlan0 modo STA
        s2 : Conecta SSID AutomaisIO
        s3 : DHCP client · obtém IP
        s4 : Verifica handshake WireGuard a cada 30s
        s5 : Plataforma Automais.IO configura WG remotamente
        s1 --> s2
        s2 --> s3
        s3 --> s4
        s4 --> s5
    }

    UNCONFIGURED --> CONFIGURED : 1º handshake WG OK\n→ persiste wifi_state.json\n(transição permanente)

    state CONFIGURED {
        direction TB
        ap : wlan0 modo AP\nSSID AeroMais-XXYYZZ\nIP 10.254.254.1/24\niptables DROP ALL (wlan0)
        relay : Sub-loop · Relay WireGuard\nvia AeromaisApp\nWhitelist dinâmica por IP
        ap --> relay : Tablet conecta ao AP
        relay --> ap : Relay encerra
    }
      

Por que o AP é aberto (sem senha WPA)?

Decisão Arquitetural — Security by Design

O AP intencionalmente não usa senha WPA porque a segurança é garantida por mecanismo superior: iptables com política DROP total. Qualquer cliente que se conectar ao AP recebe um IP, mas nenhum pacote é aceito. O acesso à rede só é liberado para o IP do dispositivo que estabelecer o túnel WireGuard de relay — e mesmo assim, somente os pacotes necessários para o relay.

Isso elimina o problema clássico de "senha WiFi anotada/esquecida" em ambientes industriais, mantendo uma segurança superior à de uma senha WPA2 estática.

Captive Portal — 302 intencional (não 204)

Por que 302 e não 204 no probe do Android?

O servidor HTTP do AP responde ao probe de conectividade do Android (GET /generate_204) com HTTP 302 (redirect), nunca com 204. A distinção é crítica para o funcionamento do relay:

Resposta ao probeInterpretação do AndroidEfeito no roteamento
204 No Content "Esta rede tem internet" ❌ Android roteia TODO o tráfego pelo WiFi. Outros apps perdem internet. Relay UDP tenta sair pelo WiFi (CPMais sem internet) → handshake WireGuard nunca completa.
302 Found "Captive portal detectado" ✅ Android mostra notificação "Fazer login no WiFi" (ignorável). Mantém o 4G como rota padrão para todos os apps e sistema. Relay UDP e tráfego do sistema continuam usando celular normalmente.

O 302 aponta para http://10.254.254.1/ onde o captive portal serve uma página informativa sobre o AP AeroMais. Ao abrir a notificação, o operador vê "Conectado ao ponto de acesso AeroMais — internet disponível via rede celular". A notificação pode ser simplesmente ignorada — a conexão WiFi ao CPMais permanece ativa e funcional.

Arquivos gerados em runtime

ArquivoDescrição
/var/lib/aeromais/wifi_state.jsonEstado persistido (unconfigured / configured)
/etc/hostapd/aeromais-ap.confConfiguração do AP hostapd
/etc/dnsmasq.d/aeromais-ap.confConfiguração DHCP para o AP
/etc/wireguard/wg-automais-ap.confConfig WireGuard de relay (gerada dinamicamente, nunca persistida entre usos)
/var/log/wifimaismanager.logLog completo do módulo

5. PKI e mTLS — Arquitetura de Certificados

O uso de IP fixo local inviabiliza Let's Encrypt (que não emite para IPs). A solução adotada é uma CA privada com emissão dinâmica via PKI Service da plataforma Automais.IO, com a chave privada da CA armazenada e gerenciada internamente pela plataforma Automais.IO.

flowchart LR
    KV["🔐 Automais.IO\nCA RSA-4096\n(gerenciada internamente)"]

    subgraph api["Plataforma Automais.IO · PKI Service"]
        CA["CA pública\n(pinning Flutter + RPi)"]
        ENR["Enrollment\nCSR + token OTP → Certificado"]
        REN["Renovação\nmTLS → Cert renovado"]
        REV["Revogação\n→ Atualiza CRL"]
        CRL["CRL\n(Certificate Revocation List)"]
    end

    PG[("Audit log\ncertificados emitidos\ntokens de enrollment")]

    KV-->|"assina"| ENR
    KV-->|"assina"| REN
    KV-->|"assina"| REV
    ENR---PG
    REN---PG
    REV---PG
      

Fluxo de Enrollment do RPi

#EtapaQuem executaSegurança
1Gera keypair RSA local (openssl req -newkey rsa:2048)RPi (install.py)Chave privada NUNCA sai do RPi
2Envia CSR + enrollment token para o PKI ServiceRPiToken OTP, TTL 15min, uso único, hash SHA-256 no banco
3Valida token + CSR, assina internamente e retorna certificadoPlataforma Automais.IORate limit por IP
4Salva server.crt, ca.crt, remove server.csrRPiPermissões 0600 nos arquivos
5Configura Nginx com mTLS usando os certificadosRPiNginx verifica CRL a cada request
6Cron job diário verifica renew_afterRPiRenovação automática sem novo token (usa mTLS)

Problemas resolvidos pela arquitetura PKI

Achado típico de pentestSeveridadeSolução nesta arquitetura
Chave privada embarcada no APKCríticaChave gerada no dispositivo, nunca transmitida
Certificado estático no APKAltaEmissão dinâmica via CSR com enrollment token
Ausência de revogaçãoAltaCRL endpoint + Nginx verifica a cada request
Sem auditoria de emissãoMédiaAudit log completo de todos os certificados emitidos, renovados e revogados
Replay de credenciaisAltaTokens OTP com TTL 15min, uso único
CA key acessível em discoCríticaChave gerenciada internamente pela plataforma Automais.IO — não exposta ao AeroMais

6. Pendências PTRZN — Visão Geral

Cada item PTRZN representa uma pendência de segurança identificada durante o desenvolvimento. As seções a seguir detalham o problema original, a solução implementada e como validar a correção.

PTRZN-1516 Resolvido Severidade Média Disclosure de Tecnologias via Response Headers
TipoInformation Disclosure / Fingerprinting
ComponenteFlask + Nginx
Data2026-04-10

Problema

As respostas HTTP incluíam o header Server: Werkzeug/3.1.5 Python/3.12.3, revelando framework, versão e runtime para qualquer atacante que inspecionasse as respostas. Isso permite pesquisa direcionada de CVEs para a versão exata.

Evidência do pentest

# Antes da correção:
HTTP/1.1 500 INTERNAL SERVER ERROR
Server: Werkzeug/3.1.5 Python/3.12.3

Solução

Remoção ativa no hook @app.after_request em security_headers.py e server_tokens off no Nginx:

# security_headers.py
if 'Server' in response.headers:
    del response.headers['Server']
if 'X-Powered-By' in response.headers:
    del response.headers['X-Powered-By']
# /etc/nginx/nginx.conf
http {
    server_tokens off;  # PTRZN-1516
}

Resultado

# Depois da correção:
HTTP/1.1 200 OK
(header "Server" ausente)
PTRZN-1517 PTRZN-1518 Resolvido Alta / Média Detecção de Root e Emulador — Android
TipoRuntime Integrity
ComponenteAeromaisApp (Flutter + Kotlin)
Data2026-04-10

Problema

O app não verificava se o dispositivo estava rooteado ou era um emulador. Dispositivos rooteados permitem interceptação via Frida/Xposed, bypass de certificate pinning e extração de dados sensíveis.

Detecção de Root — 5 camadas independentes (SecurityPlugin.kt)

#EstratégiaO que verifica
1Arquivos / bináriossu, daemonsu, paths do Magisk, KernelSU, APatch
2Apps instalados~20 pacotes: com.topjohnwu.magisk, eu.chainfire.supersu, Xposed...
3Build tagsBuild.TAGS contendo test-keys
4Sistema gravávelFile("/system").canWrite()
5Binário su via whichRuntime.exec(["which", "su"])

Detecção de Emulador — 4 camadas (SecurityPlugin.kt)

#EstratégiaO que verifica
1Propriedade ro.kernel.qemugetSystemProperty("ro.kernel.qemu") == "1"
2Build stringsBuild.MODEL, BUILD.HARDWARE, Build.FINGERPRINT
3Arquivos QEMU/dev/socket/qemud, /dev/qemu_pipe
4Fingerprint genéricaComeça com generic/unknown sem release-keys
Decisão: "google" removido da lista de emuladores
Dispositivos Google Pixel físicos têm Build.MANUFACTURER="Google". Incluir "google" na lista causaria falso positivo bloqueando usuários legítimos. A detecção AVD é coberta pelas outras três estratégias.

Comportamento por tipo de dispositivo

Tipo de DispositivoResultado
Android físico sem root✅ App inicia normalmente
Android físico rooteado (Magisk)🚫 Tela "Dispositivo Rooteado" — app encerrado
Google Pixel físico (sem root)✅ App inicia normalmente
AVD / Android Studio Emulator🚫 Tela "Emulador Detectado" — app encerrado
Genymotion / BlueStacks / NoxPlayer🚫 Tela "Emulador Detectado" — app encerrado
PTRZN-1520 Resolvido Alta Integridade do APK — Anti-tamper / Anti-debug
TipoAPK Integrity / Anti-tamper
ComponenteAeromaisApp (Flutter + Kotlin)

Problema

O APK poderia ser decompilado com apktool, modificado e reempacotado sem que o app detectasse a adulteração. Um APK adulterado pode remover verificações de segurança, injetar código malicioso ou modificar endpoints da API.

Solução

O SecurityChecker (integrado ao fluxo PTRZN-1517/1518) inclui verificação de integridade do APK. Se detectado APK adulterado (securityResult.isTampered), o mesmo fluxo de bloqueio é ativado: SecurityBlockedApp com PopScope(canPop: false).

// main.dart — executado antes de qualquer init
if (securityResult.isTampered) {
    runApp(SecurityBlockedApp(reason: 'APK Adulterado'));
    return; // nada mais é executado
}

Evidência para pentest

# Demonstrar que APK modificado é rejeitado
apktool d aeromais.apk -o apk_extracted
# Modificar qualquer arquivo e reempacotar
apktool b apk_extracted -o aeromais_mod.apk
# Instalar e executar → Tela de bloqueio "APK Adulterado"

# Demonstrar que não há chave privada embarcada
grep -r "BEGIN PRIVATE KEY" apk_extracted/
# Resultado esperado: sem saída (vazio)
PTRZN-1521 Resolvido Média Ausência de Headers de Segurança HTTP
TipoHTTP Hardening
ComponenteFlask + Nginx

Headers implementados (dupla camada Flask + Nginx)

HeaderValorAtaque mitigado
X-Frame-OptionsSAMEORIGINClickjacking via iframe
X-Content-Type-OptionsnosniffMIME type sniffing
Strict-Transport-Securitymax-age=31536000; includeSubDomains; preloadMITM / downgrade HTTP
Content-Security-Policydefault-src 'self'; ...XSS, injeção de recursos externos
Referrer-Policystrict-origin-when-cross-originVazamento de dados via Referer
Permissions-Policygeolocation, microphone, camera... bloqueadosAPIs sensíveis do browser
Cross-Origin-Embedder-Policyrequire-corpIsolamento de recursos incorporados
Cross-Origin-Resource-Policysame-originLeitura cross-origin
Cross-Origin-Opener-Policysame-originAcesso cross-origin ao objeto window
PTRZN-1522 Resolvido Média Exposição de Stack Traces em Respostas HTTP
TipoImproper Error Handling / Information Disclosure
Revisões2 (varredura completa em 2026-04-10)

Problema

Blocos except retornavam str(e) diretamente ao cliente, podendo expor caminhos de arquivo, nomes de módulos, detalhes de queries SQL e outros detalhes internos.

Evidência do pentest

# Requisição com Content-Type JSON mas body form-encoded
curl -X POST http://192.168.1.12:5000/api/auth/login \
  -H 'Content-Type: application/json' \
  --data-binary 'username=Administrador&password=admin'

# Antes: HTTP 500 (JSONDecodeError potencialmente exposto)
# Depois: HTTP 400 {"error": "Dados inválidos"}

Solução: módulo error_handler.py centralizado

  • Stack trace completo logado apenas no servidor
  • Cliente recebe apenas mensagem genérica e segura
  • Handlers globais Flask para 404, 405, 500 e exceções não tratadas
  • HTTP status codes semânticos corretos (400, 401, 403, 404, 422, 500, 503)

Arquivos corrigidos (varredura completa)

~18+ blocos except em: routes.py, auth.py, afericao.py, interface.py, wifimaismanager.py, cpmaisio-emulator.py

PTRZN-1523 Resolvido Alta Cookie de Sessão sem Flags Secure / HttpOnly / SameSite
TipoCookie Security Hardening
ComponenteFlask + config.py

Flags configuradas

FlagValorProteção
SecureTrue em produção/NginxCookie não transmitido em HTTP puro
HttpOnlyTrue sempreCookie não acessível via JavaScript
SameSiteLaxProteção CSRF

Resultado esperado no header de resposta

Set-Cookie: aeromais_session=<valor>; Secure; HttpOnly; SameSite=Lax; Path=/
Nota sobre o Flutter
O app Flutter usa webview_flutter — o WebView respeita automaticamente as flags retornadas pelo servidor. Não foi necessária nenhuma alteração no código Dart.
PTRZN-1525 Resolvido Alta Hash de Senha MD5 → bcrypt
TipoCriptografia insuficiente
Componenteauth.py / usuarios_manager.py

Problema

O hash de senha usava MD5 — algoritmo quebrado, reversível com rainbow tables e trivialmente bruteforçável em hardware moderno.

Solução

# auth.py — verificação com bcrypt puro (PTRZN-1525: MD5 removido)
return bcrypt.checkpw(senha.encode('utf-8'), result['senha'].encode('utf-8'))

O bcrypt usa salt automático e cost factor ajustável. Hashes MD5 existentes foram migrados na atualização do banco.

PTRZN-1526 Resolvido Alta Segredos Hardcoded → Variáveis de Ambiente
TipoSecrets Management
Componenteconfig.py / .env

Segredos movidos para variáveis de ambiente

SegredoVariável de ambiente
JWT secret keyJWT_SECRET
Flask session secretFLASK_SECRET_KEY
DB passwordDB_PASSWORD
Cookie flagsSESSION_COOKIE_SECURE, SESSION_COOKIE_SAMESITE

Gerenciado via python-dotenv com arquivo .env não versionado (no .gitignore).

PTRZN-1527 Resolvido Média Política de Senha Fraca — PasswordValidator
Componentepassword_validator.py

Validações implementadas

  • Comprimento mínimo e máximo (32 chars)
  • Exigência de letra maiúscula, minúscula, número e símbolo
  • Bloqueio de sequências numéricas (123, 987)
  • Bloqueio de sequências alfabéticas (abc, xyz)
  • Bloqueio de caracteres repetidos consecutivos (aaa, 111)
  • Bloqueio de padrões repetitivos (121212, ababab)
PTRZN-1528 Em Desenvolvimento Alta IDOR — Exposição de IDs Internos na API
TipoInsecure Direct Object Reference
Componenteroutes.py + config.js + afericao.js

Problema

A API retornava IDs numéricos auto-incrementais do banco ({"id": 1}), permitindo enumeração de usuários e tentativas de IDOR (DELETE /api/usuarios/1).

Solução: RefManager — referências efêmeras por sessão

Um hash de referência efêmero (HMAC-SHA256(session_secret + session_id, entity_type + real_id)), truncado em 12 caracteres, substitui os IDs reais. As refs são por sessão — inválidas em outras sessões e limpas no logout.

# ANTES — ID real exposto na URL
GET  /api/usuarios  →  [{"id": 1, "nome": "João"}, {"id": 2, "nome": "Maria"}]
DELETE /api/usuarios/1   ← ID real previsível, enumerável

# DEPOIS — ref efêmera por sessão
GET  /api/usuarios  →  [{"ref": "a3f7b2", "nome": "João"}, {"ref": "9c1d44", "nome": "Maria"}]
DELETE /api/usuarios/a3f7b2   ← válida somente nesta sessão

# No servidor:
#   resolve("a3f7b2") → real_id = 1  →  executa DELETE WHERE id = 1
#   ref de outra sessão  →  HTTP 403 Forbidden
Status Atual
O módulo ref_manager.py está implementado. A integração completa nos endpoints e frontend está em andamento.
PTRZN-1529 Resolvido Baixa Lógica de Criação de Usuário — Duplicatas e Dados Seguros

Verificação de duplicidade de usuário via tratamento de exceção no banco. Resposta de sucesso usa get_usuario_publico() que retorna apenas dados sem o hash de senha — o hash nunca é exposto na API.

# HTTP 409 Conflict se usuário já existe
# HTTP 400 Bad Request se senha não atende à política
# Resposta de sucesso: {"id": X, "nome": "...", "nivel_acesso": 2}
# (sem campo "senha")
PTRZN-1530 Resolvido Alta Sessão não Invalidada no Logout
TipoSession Management
Componentesession_manager.py + routes.py

Problema

O logout apenas chamava session.clear() no cliente. O servidor não invalidava a sessão — um atacante com o cookie poderia continuar usando-o após o logout.

Solução: SessionManager com lista de revogação server-side

  • Cada login gera um session_id único (secrets.token_urlsafe(32))
  • O SessionManager mantém dicionários de sessões ativas e revogadas em memória
  • Cada endpoint protegido verifica se a sessão foi revogada antes de processar
  • No logout: revoke_session(session_id) move a sessão para a lista de revogadas
  • Limpeza automática de dados antigos a cada 5 minutos
  • Suporte a revoke_all_user_sessions(user_id) — logout em todos os dispositivos
PTRZN-1531 Resolvido Alta XSS — Cross-Site Scripting Completo
TipoCross-Site Scripting (XSS)
ComponenteFlask routes.py + config.js + afericao.js

Pontos vulneráveis identificados

ArquivoDado expostoVetor
config.jsusuario.nomeinnerHTML com template literal
afericao.htmldado.usuarioinnerHTML com template literal
Campo nome (API)Aceita <img src=x onerror=alert()>Sem restrição de caracteres HTML

Correções em 3 camadas

CamadaCorreção
DOM (frontend)innerHTMLtextContent via função criarCelula()
Backend (routes.py)Regex ^[a-zA-Z0-9 \-_àáâã...]+$ no campo nome; max_length=20
Banco de dadosColuna nome VARCHAR(20); script de migração incluído

Limite de senha — justificativa da decisão de 32 caracteres

A senha armazena sempre o hash bcrypt (60 chars fixos). Limitar a senha a 10 chars não traz ganho de segurança. O limite de 32 chars bloqueia DoS via hashing de strings enormes, sem prejudicar passphrases (SolarSys!2026 cabe em 32).

PTRZN-1532 Resolvido Média Validação de Entrada Insuficiente — InputValidator

Módulo centralizado input_validator.py aplicado em todos os endpoints críticos. Valida tipo, comprimento mínimo/máximo, padrão regex e caracteres proibidos antes de qualquer processamento. Inclui validação de que request.get_json() não retorna None (Content-Type inválido) antes de acessar campos.

# Exemplo de uso em routes.py
input_validator = get_input_validator()
username = input_validator.validate_string(
    data.get('username', ''),
    field_name='username',
    min_length=1, max_length=20,
    pattern=r'^[a-zA-Z0-9 \-_àáâã...]+$'
)
PTRZN-1538 Resolvido Alta Ausência de Rate Limiting no Login
TipoBrute Force Protection
Componenterate_limiter.py + routes.py

Comportamento do RateLimiter

  • Conta tentativas falhadas por usuário/IP
  • Bloqueia temporariamente após N falhas consecutivas
  • Header Retry-After informa quando pode tentar novamente
  • Login bem-sucedido reseta o contador
  • Proteção contra enumeração de usuários (tempo de resposta constante)
PTRZN-1540 Feature Remote View — Visualização Remota Dual Screen Capture
Status AndroidImplementado
Status ViewerPendente (PTRZN-1541)
Data2026-04-10

Objetivo

Permitir visualização remota do app Android por suporte técnico, via VPN WireGuard, sem depender de root, modo desenvolvedor ou ADB.

Dois métodos de captura implementados

MétodoPermissãoConteúdoFallback
Método 1: RepaintBoundaryNenhumaWidget tree Flutter (PNG)Sempre disponível
Método 2: MediaProjection1 toque (padrão Android)Tela inteira (JPEG)Cai para M1 se falhar

Segurança da captura

  • Bytes ficam em memória — sem transmissão automática para lugar nenhum
  • A transmissão (PTRZN-1541) será via WireGuard + sessão Flask autenticada
  • SecurityChecker (PTRZN-1517/18) bloqueia em dispositivos rooteados antes do código de captura
  • Android exige notificação persistente para MediaProjection — operador sempre sabe que captura está ativa

7. Objetivos de Segurança — Demonstração para Pentest

Checklist de controles implementados

  • mTLS: Toda conexão ao servidor exige certificado de cliente válido (RPi/App). Conexão sem cert = 400 na camada TLS, Flask nunca é alcançado.
  • PKI dinâmica: Chave privada gerada no dispositivo. Enrollment token OTP (TTL 15min, uso único). CA key gerenciada internamente pela plataforma Automais.IO.
  • bcrypt: Senhas armazenadas com bcrypt. MD5 eliminado.
  • Secrets em variáveis de ambiente: JWT_SECRET, FLASK_SECRET_KEY, DB_PASSWORD — nunca hardcoded.
  • Session invalidation: Logout revoga sessão server-side. Cookie antigo é inválido imediatamente.
  • Cookie flags: Secure + HttpOnly + SameSite=Lax em produção.
  • Security headers: CSP, HSTS, X-Frame-Options, CORP, COEP, COOP em dupla camada (Flask + Nginx).
  • Rate limiting: Brute force bloqueado no login.
  • XSS: innerHTML → textContent; regex no campo nome; maxlength em 3 camadas (DOM, backend, banco).
  • Error handling: Stack traces nunca expostos ao cliente. Detalhes logados apenas no servidor.
  • Input validation: Centralizada, com tipo, comprimento, padrão e Content-Type validation.
  • Server fingerprinting: Headers Server e X-Powered-By removidos. server_tokens off no Nginx.
  • Root/emulator detection: 5+4 estratégias independentes; bloqueio total antes de qualquer código sensível.
  • APK integrity: Anti-tamper verificado na inicialização.
  • Renovação de certificados: Automática no RPi (cron) e no app Flutter (na abertura).
  • CRL: Nginx verifica CRL a cada request. CRL atualizada imediatamente após revogação.
  • Relay UDP estável — dual-socket: _socketLocal (WiFi AP) recebe do CPMais; _socketRemote (IP celular) fala com automais.io. Handshake WireGuard completa mesmo quando o Android rotearia o tráfego via WiFi.
  • 4G mantido para sistema e outros apps: Captive portal retorna 302 ao probe /generate_204. Android mantém celular como rota padrão de internet — outros apps e o SO não perdem conectividade ao conectar no AP do CPMais.
  • ⚠️ IDOR (PTRZN-1528): RefManager implementado — integração em andamento.

8. Dificuldades Encontradas no Processo de Segurança

8.1 Ambiente de Desenvolvimento vs. Produção

Um dos maiores desafios foi garantir que controles como SESSION_COOKIE_SECURE=True e HSTS ficassem ativados apenas em produção — em desenvolvimento local com HTTP puro, essas flags quebram o fluxo. A solução foi lógica condicional em config.py: detectar automaticamente se NGINX_DOMAIN está definido (indicador de produção) sem precisar setar ENVIRONMENT=production manualmente.

8.2 Cobertura Total de Error Handling

A primeira rodada do PTRZN-1522 corrigiu os blocos mais óbvios. Somente uma varredura manual completa (revisão 2) revelou pontos adicionais que retornavam str(e): em reiniciar_abastecimento, no emulador SPI, nas rotas do encoder e no WiFiManager. Isso ilustra a dificuldade de garantir cobertura total sem uma política de "proibido usar str(e) em responses" testada por lint.

Lição aprendida
Validar ausência de stack trace em qualquer response exige varredura automatizada. Recomenda-se criar um teste de integração que verifica se qualquer resposta de erro contém palavras como Traceback, File "..., Exception:.

8.3 Falso Positivo em Detecção de Emulador

A lista inicial de fabricantes de emuladores incluía "google", causando bloqueio em dispositivos Pixel físicos durante testes. A correção (remover "google" da lista) exigiu análise cuidadosa de quais estratégias de detecção alternativas cobriam o AVD sem afetar hardware real.

8.4 Garantir que mTLS se aplica a TODOS os endpoints

O Nginx com ssl_verify_client on garante mTLS globalmente. O risco estava em endpoints Flask que poderiam ser alcançados diretamente (se o Flask escutasse em 0.0.0.0). A correção (PTRZN-1515) foi garantir que Flask escuta apenas em 127.0.0.1, tornando o Nginx o único ponto de entrada.

8.5 AP WiFi sem senha como vetor de ataque percebido

A decisão de manter o AP sem senha WPA é contraintuitiva e provavelmente será questionada pela equipe de pentest. A justificativa técnica — iptables DROP ALL é superior a uma senha WPA estática em ambiente industrial — precisa ser demonstrada com evidências das regras de firewall ativas, não apenas documentada.

8.6 IDOR com IDs auto-incrementais

A solução RefManager (PTRZN-1528) exigiu refatoração do frontend e do backend em paralelo. O desafio foi garantir que a camada de resolução de refs fosse transparente para o WebView (que roda JavaScript servido pelo backend) sem quebrar a funcionalidade existente. Esta é a única pendência ainda em desenvolvimento.

8.7 Roteamento Android — WiFi vs. 4G no AP do CPMais

Um comportamento OEM não-padronizado causou instabilidade no relay por semanas: alguns dispositivos Android, ao se conectar a um AP sem internet e confirmar "conectar mesmo assim", passam a rotear todo o tráfego pelo WiFi — incluindo os pacotes UDP do relay. Como o CPMais não tem saída para a internet, os pacotes eram silenciosamente descartados no RPi.

O problema se manifestava como: relay conecta → conta 1 pacote recebido + 1 enviado → para → status cai para "aguardando" → tenta novamente após 60 s. Esse ciclo repetia indefinidamente sem nunca completar o handshake WireGuard.

A causa raiz era dupla: (1) roteamento pelo WiFi, e (2) o dnsmasq com address=/#/10.254.254.1 fazia automais.io resolver para 10.254.254.1, tornando o relay incapaz de distinguir pacotes do CPMais de respostas do servidor remoto.

Solução implementada
Três mudanças em conjunto resolveram o problema: captive portal retorna 302 (mantém 4G como rota padrão), relay usa dual-socket separando WiFi de celular, e DNS de automais.io é resolvido via ConnectivityManager da rede celular, ignorando o dnsmasq do AP.

8.8 Dificuldade de demonstrar segurança "máxima" para pentest

Demonstrar que objetivos de segurança foram atingidos não é trivial porque:

  • Segurança negativa — provar que algo não acontece é mais difícil que provar que algo acontece
  • Profundidade de camadas — mTLS + sessão + rate limiting + bcrypt + cookie flags são interdependentes; testar uma camada isoladamente não simula o ataque real
  • Ambiente embarcado — certificados para IPs locais, WireGuard, AP sem senha — cada elemento é justificável mas incomum, podendo ser marcado como "finding" antes de entender o contexto
Recomendação para apresentação ao pentest
Fornecer este documento antes da reunião de kick-off do pentest, para que a equipe entenda o contexto de cada decisão arquitetural antes de emitir findings. Isso evita falsos positivos em itens intencionais (AP sem senha, por exemplo).

9. Evidências para o Relatório de Pentest

9.1 mTLS — rejeição de conexão sem certificado

# Deve retornar erro SSL (400 no Nginx)
curl -v https://<IP_RPI>/api/configuracoes
# Esperado: SSL handshake error — 400 No required SSL certificate was sent

# Com certificado de cliente — deve funcionar
curl -v \
  --cacert certs/ca.crt \
  --cert certs/client.crt \
  --key certs/client.key \
  https://<IP_RPI>/api/configuracoes

9.2 Enrollment token de uso único

# A plataforma Automais.IO valida que cada token só pode ser usado uma vez.
# Um segundo uso do mesmo token deve retornar 401 — verificável no log do RPi:
# install.py: "Enrollment falhou: 401 — token inválido, expirado ou já usado"

# Evidência indireta no RPi:
sudo cat /etc/aeromais/certs/.renewal_info.json
# Deve conter issued_at e expires_at do certificado emitido com sucesso

9.3 Stack trace não exposto

# Content-Type JSON com body form-encoded
curl -X POST https://<IP_RPI>/api/auth/login \
  -H 'Content-Type: application/json' \
  --data-binary 'username=Administrador&password=admin'
# Esperado: HTTP 400 {"error": "Dados inválidos"}
# NÃO deve aparecer: Traceback, File ", Exception, str(e)

9.4 Cookie com flags de segurança

# Verificar header Set-Cookie no login
curl -v -X POST https://<IP_RPI>/api/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"..."}'
# Esperado: Set-Cookie: aeromais_session=...; Secure; HttpOnly; SameSite=Lax

9.5 Sessão invalidada após logout

# 1. Fazer login e guardar o cookie
# 2. Fazer logout
# 3. Tentar usar o cookie antigo em qualquer endpoint protegido
# Esperado: HTTP 401 {"authenticated": false}

9.6 Rate limiting no login

# Tentar login com senha errada repetidamente
for i in {1..10}; do
  curl -X POST https://<IP_RPI>/api/auth/login \
    -H 'Content-Type: application/json' \
    -d '{"username":"admin","password":"errada"}'
done
# Após N tentativas: HTTP 429 Too Many Requests
# Header Retry-After presente na resposta

9.7 Ausência de chave privada no APK

apktool d aeromais.apk -o apk_extracted
grep -r "BEGIN PRIVATE KEY" apk_extracted/
grep -r "BEGIN RSA PRIVATE KEY" apk_extracted/
# Resultado esperado: sem saída (vazio)

9.8 Headers de segurança presentes

curl -I https://<IP_RPI>/
# Devem aparecer:
# X-Frame-Options: SAMEORIGIN
# X-Content-Type-Options: nosniff
# Strict-Transport-Security: max-age=31536000...
# Content-Security-Policy: default-src 'self'...
# Cross-Origin-Embedder-Policy: require-corp
# Cross-Origin-Resource-Policy: same-origin
# Cross-Origin-Opener-Policy: same-origin
# NÃO deve aparecer: Server: (com versão)

9.9 Firewall iptables no AP WiFi

# No RPi em modo AP, verificar regras ativas
sudo iptables -L INPUT -n -v | grep wlan0
# Esperado: policy DROP para wlan0
# Verificar que somente IPs whitelistados têm ACCEPT
sudo iptables -L INPUT -n -v | grep ACCEPT

9.10 Relay UDP — dual-socket e 4G isolado

# 1. Conectar tablet ao AP AeroMais-XXYYZZ
# 2. Confirmar "conectar sem internet" se solicitado
# 3. No AeromaisApp, abrir tela do relay UDP
#
# Verificar que o relay ativou dual-socket:
#   - "dual-socket: WiFi 10.254.254.x:51821 → automais.io(IP_REAL):51820 via IP_CELULAR"
#
# 4. Em outro app do tablet (ex: browser), acessar qualquer site
#    → deve carregar normalmente (internet via 4G ativa)
#
# 5. Aguardar handshake WireGuard no log do RPi:
#    sudo journalctl -u wifimaismanager -f
#    → "Relay ativo: 10.254.254.X — rota padrão e whitelist aplicadas"
#    → "Handshake wg-automais-ap confirmado via 10.254.254.X"

# Verificar que 302 é retornado pelo captive portal (não 204):
curl -v http://10.254.254.1/generate_204
# Esperado: HTTP/1.1 302 Found
# Location: http://10.254.254.1/
# (NÃO deve retornar 204 — 204 causaria roteamento via WiFi)

10. Pendências Abertas e Próximos Passos

ItemPTRZNDescriçãoPrioridade
IDOR / RefManager 1528 Integração completa do RefManager em todos os endpoints e frontend. Módulo implementado, integração em andamento. Alta
Remote View Viewer 1541 Implementar o viewer do remote view (WebSocket/WebRTC + autenticação Flask + frontend HTML). Side Android já implementado. Média
Play Integrity API 1517 Camada adicional de atestação via Google para cobertura de Magisk DenyList (quando Google Play Services disponível). Baixa
CSP sem unsafe-inline 1521 Migrar para nonces ou hashes CSP, eliminando 'unsafe-inline' e 'unsafe-eval' da política. Baixa
Lint para str(e) em responses 1522 Criar teste automatizado que verifica ausência de Traceback / str(e) em respostas de erro de integração. Média
HSTS Preload 1521 Submeter domínio ao HSTS Preload List (requer max-age ≥ 1 ano em produção estável). Baixa
Conclusão
O AeroMais implementa um conjunto abrangente de controles de segurança em múltiplas camadas, cobrindo as principais categorias do OWASP Mobile Top 10 e Web Top 10. A única pendência crítica aberta (PTRZN-1528 IDOR) tem o módulo de solução implementado e está em fase de integração. Todos os demais objetivos de segurança podem ser demonstrados com os procedimentos de validação documentados neste relatório.