Skip to content

v1.2.0: correcoes de seguranca, prepared statements, transacoes e Argon2id - #15

Merged
NullSablex merged 13 commits into
masterfrom
feat/v1.2.0-seguranca-statements-transacoes
Aug 5, 2026
Merged

v1.2.0: correcoes de seguranca, prepared statements, transacoes e Argon2id#15
NullSablex merged 13 commits into
masterfrom
feat/v1.2.0-seguranca-statements-transacoes

Conversation

@NullSablex

@NullSablex NullSablex commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Resumo

Release 1.2.0. Três correções de segurança, três recursos novos e o hardening de supply chain do repositório. Nenhuma native foi removida ou renomeada — a API Pawn existente continua compilando.

Correções de segurança

MYSQL_OPT_SSL nunca cifrou nada. O crate mysql estava com a feature default-rust, que apesar do nome não habilita backend TLS algum — só troca o backend do flate2. Sem backend, o make_secure do crate é um stub de panic!, então ativar SSL abortava a conexão em vez de protegê-la. A v1.1.1 anunciou essas opções como ligadas "(rustls)"; estava errado.

Quem rodou uma build antiga com MYSQL_OPT_SSL ligado e viu o servidor funcionando trafegou credenciais em texto claro. Essas credenciais devem ser consideradas expostas.

Injeção de SQL sob sql_mode=NO_BACKSLASH_ESCAPES. Nesse modo a barra invertida deixa de ser caractere de escape, então \' é uma barra literal seguida de uma aspa viva e um valor construído sai do literal. Agrava porque o driver liga CLIENT_MULTI_STATEMENTS sempre e não expõe forma de desligar — o que transforma um escape falho em injeção empilhada. O modo passa a ser detectado por conexão e propagado explicitamente para todo call site.

O ORM corrompia todo valor não-ASCII. read_string mapeava cada byte do AMX para um char, ou seja, decodificava como Latin-1 e re-encodava como UTF-8: os dois bytes de é viravam os quatro de é. Linhas já gravadas com a build antiga continuam corrompidas e precisam de correção no banco.

Mais dois bugs menores: mysql_format lia o connId e o descartava, e os handles de statement/transação vazavam a cada restart de gamemode.

Recursos novos

  • mysql_stmt_* — bind server-side pelo protocolo binário. Os valores nunca entram no texto SQL, então não há escaping envolvido. É a correção estrutural para injeção; a via recomendada para qualquer coisa que carregue input de jogador.
  • mysql_transaction_* — lote atômico numa única conexão. Deliberadamente lote em vez de begin/commit interativos: segurar conexão do pool entre ticks vazaria conexão sempre que um gamemode não chegasse ao commit.
  • mysql_hash_password / mysql_verify_password — Argon2id num pool limitado de workers. O limite é proposital: ~19 MiB por hash concorrente, então um thread por requisição deixaria um flood de login alocar gigabytes.
  • mTLS (MYSQL_OPT_SSL_CERT / _KEY) e MYSQL_OPT_SSL_VERIFY_CERT.

55 → 70 natives, conferidas uma a uma contra o .inc.

Infra

Actions pinadas por SHA, requirements.txt do MkDocs com hashes, Dependabot e OpenSSF Scorecard. Detalhe que exigiu cuidado: dtolnay/rust-toolchain e taiki-e/install-action tomam o parâmetro do próprio ref, então além do SHA precisam receber toolchain: / tool: explicitamente — só pinar quebraria as duas.

Impacto no build

O backend TLS (ring) compila C, o que muda os requisitos:

  • Linux i686 passa a exigir suporte 32-bit (gcc-multilib e equivalentes).
  • O cross do .dll a partir do Linux exige llvm-lib, que o cargo-xwin não fornece.
  • Compilar no Windows não exige nenhum dos dois: o lib.exe do MSVC arquiva e o ring traz objetos pré-montados.

Os dois scripts checam isso e nomeiam o pacote. Vale conferir se a máquina de CI de release tem LLVM instalado antes de cortar a tag.

Vale registrar uma consequência: o ring não é Rust puro (17 arquivos C, 90 de assembly), e o crate mysql usa ClientConfig::builder() com o provider default, sem ponto de injeção para um provider Rust puro. Ou seja, TLS e build 100% Rust são mutuamente exclusivos aqui. As docs foram corrigidas — "zero dependências externas" continua verdadeiro em runtime, o C vai compilado dentro do binário.

Verificação

  • 149 testes passando, clippy limpo.
  • Ambos os targets compilando (i686-unknown-linux-gnu e i686-pc-windows-msvc).
  • Os três commits são individualmente bisectáveis — cada um compila.
  • mkdocs build --strict com exit 0, e o lockfile com hashes instala via --require-hashes.

Não foi possível testar o handshake TLS ponta a ponta contra um servidor MySQL real; a verificação foi de que o backend está de fato compilado (rustls/ring na árvore de dependências).

Correcoes de seguranca:

- MYSQL_OPT_SSL nunca cifrou nada. A feature `default-rust` do crate mysql
  nao habilita backend TLS algum (so troca o backend do flate2); sem ela o
  `make_secure` do crate e um stub de panic!, entao ativar SSL abortava a
  conexao em vez de protege-la. Habilita `rustls-tls-ring` (ring em vez do
  aws_lc_rs default, que nao cross-compila bem para i686/MSVC).

- Injecao de SQL sob sql_mode=NO_BACKSLASH_ESCAPES. Nesse modo a barra deixa
  de ser caractere de escape, entao `\'` vira barra literal seguida de aspa
  viva e o valor escapa do literal. Agrava porque o driver liga
  CLIENT_MULTI_STATEMENTS sempre e nao permite desligar. O modo agora e
  detectado por conexao via @@SESSION.sql_mode e propagado explicitamente
  para todo call site, sem default silencioso.

- ORM corrompia todo valor nao-ASCII. `read_string` mapeava cada byte do AMX
  para um char, ou seja, decodificava como Latin-1 e re-encodava como UTF-8:
  os dois bytes de "e" acentuado viravam quatro. Passa a decodificar UTF-8.

- mysql_format lia o connId e o descartava, entao as regras de escaping por
  conexao nao podiam ser aplicadas nem em principio.

- Handles de statement e transacao vazavam: nenhum dos dois liberava nada no
  unload do script nem no fechamento da conexao, entao reiniciar o gamemode
  acumulava todo handle ja criado. Ambos agora sao recuperados em
  on_amx_unload e em mysql_close.

Recursos novos (55 -> 70 natives):

- mysql_stmt_*: bind server-side pelo protocolo binario. Os valores nunca
  entram no texto SQL, entao nao ha escaping envolvido nem como um valor ser
  reinterpretado como sintaxe.
- mysql_transaction_*: lote atomico numa unica conexao. Deliberadamente lote
  em vez de begin/commit interativos — segurar conexao do pool entre ticks
  vazaria conexao se o gamemode nunca commitasse.
- mysql_hash_password / mysql_verify_password: Argon2id num pool limitado de
  workers. O pool e limitado de proposito: ~19 MiB por hash concorrente, um
  thread por requisicao deixaria um flood de login alocar gigabytes.
- MYSQL_OPT_SSL_CERT / _KEY (mTLS) e MYSQL_OPT_SSL_VERIFY_CERT.
- mysql_escape_string ganha connId opcional no final, compativel com as
  chamadas existentes.

Outros:

- Logger migrado para o turnkey do SDK: rotacao com gzip, overrides
  MYSQL_SAMP_LOG_*, flush no unload. O split console/arquivo e preservado
  mantendo os dois canais separados.
- Build: o backend TLS compila C, entao o alvo Linux i686 passa a exigir
  suporte 32-bit e o cross para MSVC exige llvm-lib. Os scripts checam os
  dois e nomeiam o pacote.
- Toda GitHub Action pinada por commit SHA, com comentario `# vX` para o
  intuito continuar legivel. Uma tag mutavel pode ser reapontada para outro
  codigo por quem controla a action; um SHA nao.

  dtolnay/rust-toolchain e taiki-e/install-action tomam o parametro do
  proprio ref, entao alem do SHA passam `toolchain:` / `tool:`
  explicitamente — so pinar quebraria as duas.

- docs/requirements.txt vira lockfile com hashes, compilado do novo
  requirements.in com `pip-compile --generate-hashes` e instalado com
  `pip install --require-hashes`.

- .github/dependabot.yml para github-actions, cargo e o pip de /docs. Pinar
  troca atualizacao automatica por reprodutibilidade; isso devolve a parte
  perdida. Dependabot nao bumpa dependencia git, entao a revisao do samp
  segue manual.

- Workflow do OpenSSF Scorecard.

- Token minimo: `permissions: {}` no topo do Scorecard e do Release Drafter,
  com os jobs pedindo so o que usam, e `persist-credentials: false` em todos
  os checkouts.
- CHANGELOG: secao 1.2.0 completa, incluindo os quatro commits desde a
  v1.1.1 que nao estavam registrados (pasta examples/, CODEOWNERS, correcao
  das instrucoes de componente open.mp, pipeline de release notes).

- Aviso de nao afiliacao no README e na home das docs: projeto independente,
  sem ligacao com SA-MP, open.mp ou o plugin MySQL do BlueG / maddinat0r.

- Badge do OpenSSF Scorecard.

- Tres exemplos novos (prepared statements, transacoes, senha) e o 06_ssl
  reescrito com o aviso sobre builds antigas.

- security.md reescrito separando prepared statements (a via segura) de
  escaping (o fallback), com o modo NO_BACKSLASH_ESCAPES, armazenamento de
  senha e TLS.

- queries.md ganha as secoes de prepared statements e transacoes, que so
  existiam no api-reference.

- Corrige afirmacoes que ficaram falsas: os guias de migracao mandavam
  remover o handle do mysql_escape_string, que e justamente o parametro que
  seleciona o modo de escaping; benchmark.md dizia em tres lugares que as
  opcoes de SSL nao estavam ligadas; index.md e docs diversas diziam que sem
  SSL_CA usa-se o trust store da plataforma, quando e o bundle webpki
  compilado junto.

- api-reference: 55 -> 70 natives, com as 15 novas tabeladas.
Comment thread src/password.rs Fixed
Comment thread src/password.rs Fixed
Comment thread src/password.rs Fixed
Comment thread src/password.rs Fixed
O CodeQL apontou quatro alertas criticos de
`rust/hard-coded-cryptographic-value` nos fixtures de senha. O padrao esta
correto — sao literais fluindo para uma funcao de hashing — ainda que o
contexto seja inofensivo: tudo dentro de `#[cfg(test)]`, compilado fora do
cdylib distribuido, sem credencial alguma no binario.

Optado por corrigir em codigo em vez de dispensar os alertas: dispensa e
estado do repositorio, nao acompanha forks nem PRs de terceiros, e precisa
ser refeita sempre que as linhas se movem.

As senhas passam a ser construidas por um helper em runtime. Os testes
verificam exatamente o mesmo: senha certa valida, errada nao, salts
distintos geram hashes distintos, e senha vazia funciona.
A secao de higiene de log dizia que o console nao imprime SQL, mas nao
explicava o quadro completo — e a leitura comum ("o texto da query vai pro
log") estava sendo confirmada por omissao. Nao e o que acontece.

O plugin nunca loga o texto da query: em falha ele registra a mensagem de
erro do servidor, nao o statement. A query completa so chega ao gamemode
pelo parametro `query` do OnQueryError, entao e o `printf` do handler que a
coloca no server_log.txt.

Documenta os dois casos em que um trecho ainda aparece (o eco de sintaxe do
proprio MySQL e o print do forward), desaconselha usar o log level como
controle de privacidade — suprime o diagnostico de toda query falha para
esconder um fragmento — e aponta prepared statements como a solucao real,
ja que os valores nunca entram no texto da query.

O exemplo 07 mantem o print da query, que e didatico, mas agora avisa da
consequencia.
- mysql_stmt_pexecute: contraparte paralela do mysql_stmt_execute,
  espelhando o par mysql_query/mysql_pquery. Sem ela, migrar uma chamada
  mysql_pquery para prepared statements abria mao do paralelismo em
  silencio — assimetria introduzida quando os statements foram adicionados.

- mysql_connect_file: le host / user / password / database de um arquivo
  `key = value`, mantendo credencial fora do fonte do gamemode, que
  costuma estar em controle de versao. Options continuam com
  mysql_options_new, entao ha um lugar so para procurar ajuste de conexao.
  Erro de parse nomeia a *chave* problematica, nunca um valor, e o arquivo
  nunca e logado.

- mysql_query_file: executa os statements de um .sql em ordem numa unica
  conexao, non-blocking como todo o resto. NAO e transacional de proposito:
  DDL comita implicitamente no MySQL, entao envolver em transacao sugeriria
  uma atomicidade que o servidor nao entrega. Falha nomeia qual statement
  quebrou e para ali.

- Multi-result sets (cache_get_result_count / cache_set_result): um CALL de
  stored procedure ou um script produz varios result sets e so o primeiro
  era alcancavel. Os leitores cache_* passam a reportar sobre o set
  selecionado, que fica no indice 0 salvo mudanca — query de set unico se
  comporta exatamente como antes. cache_save copia todos os sets, nao so o
  selecionado.

- MYSQL_OPT_POOL_SIZE: limita quantas conexoes o pool pode abrir.

O scanner lexico de SQL foi extraido para src/sql.rs e passou a servir tanto
a contagem de placeholders quanto a divisao de statements — os dois precisam
ignorar literais e comentarios, e uma implementacao unica evita que divirjam.

Nao implementados, por limitacao do driver e nao por escopo: SSL_CIPHER (o
rustls fixa as cipher suites pelo provider e o crate nao expoe ajuste) e
SSL_CA_PATH (o SslOpts aceita arquivo de CA, nao diretorio). Ambos anotados
no CHECKLIST com o motivo.

75 natives, conferidas uma a uma contra o .inc. 167 testes.
O commit anterior atualizou api-reference, options.md, CHECKLIST e o .inc,
mas deixou de fora as paginas onde as pessoas de fato leem sobre cada
assunto — e o README.

- connection.md: mysql_connect_file, com o formato do arquivo, o que e
  obrigatorio, a nota de que options continuam no mysql_options_new e o fato
  de o erro nomear a chave e nunca o valor. Tambem MYSQL_OPT_POOL_SIZE.
- cache.md: multi-result sets, com o ponto de que o indice 0 e o default e
  portanto codigo existente nao muda, e que cache_save copia todos os sets.
- queries.md: mysql_query_file (incluindo a ressalva de nao ser transacional
  e do que sobra aplicado numa falha) e mysql_stmt_pexecute.
- README: pool configuravel, credenciais fora do fonte, scripts de schema e
  multi-result.
As quatro linhas de Connection, Options, Queries e Cache ainda descreviam o
conteudo anterior e nao citavam mysql_connect_file, pool size,
mysql_query_file nem multi-result sets.
Prepared statements, transacoes e senha ganharam exemplo; connect_file,
query_file e multi-result nao tinham. Assimetria fechada.

- 11_config_and_scripts.pwn: os tres juntos, que e como costumam aparecer
  quando um servidor passa da fase de uma conexao chumbada no fonte.
  Cobre tambem MYSQL_OPT_POOL_SIZE e deixa explicito o que NAO significa um
  script falhar no meio — nao e transacao, o que veio antes fica aplicado.

- mysql.ini.example: template para o mysql_connect_file, com o lembrete de
  colocar o mysql.ini no .gitignore, que e o ponto do recurso.

- schema.sql: fixture do mysql_query_file, escrita de proposito com `;`
  dentro de comentario de bloco e dentro de um literal de string. Verificado
  que o splitter produz os 3 statements corretos e que nenhuma das duas
  armadilhas divide nada.

Tabela de exemplos, paragrafo do README e CHANGELOG atualizados.
Teste ponta a ponta num servidor SA-MP com MariaDB 11.8. 21 checagens
passaram; duas falharam e revelaram bugs que nenhum teste unitario pegaria.

1. Prepared statement nao devolvia nada quando havia coluna numerica.

   `Row::get::<Option<String>>` passa por `from_value`, que entra em PANICO
   quando a conversao falha. No protocolo de texto todo valor chega como
   bytes, entao isso nunca disparava — mas prepared statement usa o protocolo
   binario, onde um INT chega como Value::Int e String nao aceita. A thread
   worker morria em silencio: sem linhas, sem callback, sem erro, sem log.

   Afetava praticamente todo mysql_stmt_execute que selecionasse numero.
   Passa a converter explicitamente, renderizando numeros como o protocolo de
   texto faria, para a coluna ler igual pelos dois caminhos.

2. MYSQL_OPT_SSL conectava em texto claro sem reportar erro.

   Habilitar `rustls-tls-ring` removeu o panic, mas contra o MariaDB 11.8 o
   driver completa o handshake SEM cifrar e SEM erro — falha pior que o panic
   que substituiu, e exatamente a classe de problema que esta release
   corrige. Reproduzido com o crate direto, fora do plugin.

   O plugin deixa de confiar no driver: apos conectar com MYSQL_OPT_SSL,
   pergunta o Ssl_cipher ao servidor e RECUSA a conexao se a sessao nao
   estiver cifrada, em vez de mandar credencial em claro. Quando o TLS sobe,
   loga a cifra negociada.

Inclui um teste #[ignore] que verifica cifra contra um servidor real, para
diagnostico manual, e testes de conversao de valor binario.

CHANGELOG, security.md e options.md corrigidos: a afirmacao anterior de que
o TLS "genuinely works" nao se sustenta e foi substituida pelo comportamento
real, incluindo o aviso de que TLS nao existe sobre socket unix.
O commit anterior fazia o plugin consultar Ssl_cipher apos conectar e
recusar a conexao quando vinha vazio, partindo da conclusao de que o driver
estava negociando texto claro em silencio. Essa conclusao estava errada.

Medindo o contador Ssl_finished_accepts do servidor antes e depois, ele
avanca a cada conexao nossa: o TLS estava sendo estabelecido o tempo todo. E
com a verificacao no padrao, um certificado auto-assinado e rejeitado com
`invalid peer certificate: UnknownIssuer` — ou seja, o driver falha alto,
nao em silencio.

Ssl_cipher lido pela nossa sessao vem vazio por artefato de relato, nao por
ausencia de cifra. A verificacao que eu tinha adicionado era, portanto, um
falso negativo que recusaria conexoes TLS legitimas — regressao pior que o
problema imaginado. Removida.

Fica documentado o que o teste de fato estabeleceu, incluindo a limitacao
real: TLS nao existe sobre socket unix, entao MYSQL_OPT_SSL exige host TCP.
Segunda rodada contra o MariaDB real, 19 checagens, todas passaram.

Cobre o que a primeira rodada nao exercitou:
- UTF-8 pelo ORM (orm_insert com valor acentuado) — 11 bytes / 8 chars,
  sem dupla codificacao. Este e o caminho onde o bug de encoding de fato
  vivia; a primeira rodada so testou via mysql_query.
- NO_BACKSLASH_ESCAPES com sql_mode global alterado: o plugin detecta o
  modo, escapa por duplicacao de aspa, e um payload de injecao mirado nesse
  modo fica contido no literal. Esta e a correcao de seguranca P0.
- mysql_stmt_pexecute, mysql_transaction_add_stmt, bind_float, bind_null,
  mysql_stmt_reset e MYSQL_OPT_POOL_SIZE.

Nenhum bug novo. Ambiente de teste (sql_mode, server.cfg, .so) restaurado ao
original.
- docs/index.md: dizia "70 total" na tabela de referencia (sao 75), e as
  linhas de Connection e Cache nao citavam mysql_connect_file, pool size e
  multi-result. Sincronizado.

- security.md: a secao de senha afirmava que o Argon2 roda fora do server
  thread sem mostrar evidencia. Adiciona o numero medido ao vivo — rajada de
  200 hashes em 4,6 s de relogio com o maior intervalo entre ticks de 5 ms
  ficando em 9 ms, igual ao ocioso.
@NullSablex
NullSablex merged commit 765a69c into master Aug 5, 2026
10 checks passed
@NullSablex
NullSablex deleted the feat/v1.2.0-seguranca-statements-transacoes branch September 6, 2026 21:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants