Skip to content

fix(resources): stop exposing author email in page props - #1

Open
Guajir0-code wants to merge 1 commit into
sourcevortex:mainfrom
Guajir0-code:fix/author-email-exposure
Open

fix(resources): stop exposing author email in page props#1
Guajir0-code wants to merge 1 commit into
sourcevortex:mainfrom
Guajir0-code:fix/author-email-exposure

Conversation

@Guajir0-code

Copy link
Copy Markdown

Problema

O endereço de e-mail de cada autor é servido no HTML de todas as páginas que listam posts — home, categoria, tag, autor, arquivo e post interno. Qualquer visitante lê os e-mails com um "ver código-fonte", sem login e sem ferramenta nenhuma.

Evidência

Verificado em https://beta.sourcevortex.com.br/ no dia da abertura deste PR. Lendo o data-page do Inertia direto do DOM:

JSON.parse(document.getElementById('app').dataset.page).props.posts.data[0].author
{
  "ID": 2,
  "display_name": "Mayron Câmara",
  "user_nicename": "mayron",
  "user_email": "ma***@gmail.com"
}

A string user_email aparece 15 vezes no HTML da home (10 posts da listagem + 5 destaques do carrossel).

O front-end não usa esse campo. O contrato declarado em resources/js/types/index.d.ts é:

export interface PostAuthor {
    ID: number;
    display_name: string;
    user_nicename: string;
}

Ou seja: o dado é enviado, ninguém consome, e ele expõe o endereço pessoal de quem escreve no site.

Causa

app/Http/Resources/WpPostResource.php:41 serializa o model inteiro:

'author' => $this->author,

A relação em app/Models/WpPost.php:33 seleciona o e-mail de propósito:

return $this->belongsTo(WpUser::class, 'post_author', 'ID')->select([
    'ID', 'display_name', 'user_nicename', 'user_email',
]);

E WpUser não define $hidden, então nada filtra o campo na serialização. O user_email é legitimamente necessário — WpAuthorService::getAvatar() usa ele para montar o hash do Gravatar — só não deveria chegar ao navegador.

Solução

1. Enviar apenas os campos que o front declara (WpPostResource):

'author' => $this->getAuthor(),
private function getAuthor(): ?array
{
    if (! $this->author) {
        return null;
    }

    return [
        'ID' => $this->author->ID,
        'display_name' => $this->author->display_name,
        'user_nicename' => $this->author->user_nicename,
    ];
}

2. $hidden no WpUser como segunda barreira:

protected $hidden = ['user_pass', 'user_email', 'user_activation_key'];

$hidden afeta só toArray()/toJson() — o acesso por atributo continua funcionando, então getAvatar() segue lendo $user->user_email normalmente. É o que impede que um futuro 'author' => $model volte a vazar.

Por que não simplesmente tirar user_email do select()

Foi a primeira alternativa que considerei e ela quebra o avatar: WpAuthorService::getAvatar() precisa do e-mail carregado na relação para gerar o hash do Gravatar na página do post. Manter o campo carregado e controlar o que sai na resposta preserva o comportamento atual.

Como validar

php vendor/bin/pest --filter=WpPostResourceTest
✓ it does not expose the author email in the payload
✓ it keeps the fields the frontend consumes
✓ it returns null when the post has no author, as before
✓ it hides credentials when a WpUser is serialized directly

Tests: 4 passed (10 assertions)

Os testes foram verificados nos dois sentidos — rodando contra o código antes da correção, 2 deles falham como esperado:

⨯ it does not expose the author email in the payload
⨯ it hides credentials when a WpUser is serialized directly

Em produção, depois do deploy, o payload não deve mais conter a string user_email:

curl -s https://beta.sourcevortex.com.br/ | grep -c user_email   # esperado: 0

Impacto

  • Comportamento: nenhuma mudança visível. O front nunca leu user_email; os três campos que ele declara continuam idênticos.
  • Autor ausente: paridade exata com o comportamento atual — antes, um post_author órfão serializava null; continua null, agora sem risco de erro no servidor.
  • Schema: nenhuma alteração de banco.
  • Compatibilidade: nenhuma variável de ambiente nova.
  • Rollback: reverter o commit; não há migração nem estado a desfazer.

Nota sobre phpunit.xml

O phpunit.xml define DB_DATABASE=testing sem DB_CONNECTION, o que faz a suíte abortar antes de rodar qualquer teste:

QueryException: Database file at path [testing] does not exist.

Este PR fixa sqlite / :memory: para que a suíte consiga iniciar — sem isso o teste de regressão acima não roda em lugar nenhum, nem no CI. É a mudança mínima para tornar o PR verificável, não uma reforma do ambiente de testes.

Fora de escopo

  • Tests\Feature\ExampleTest continua falhando. É pré-existente e não relacionado: ele faz GET /, que exige as tabelas wp_*, inexistentes no ambiente de teste. Antes deste PR a suíte nem chegava a executá-lo. A infraestrutura de fixtures que resolve isso vem em PR separado.
  • wpAdmin.user.email. O HandleInertiaRequests::share() também expõe o e-mail do administrador, por um caminho diferente (WpAuthService). Não mexi aqui para manter este PR focado; será tratado à parte.
  • As tabelas criadas no beforeEach do teste são o recorte mínimo do schema do WordPress que este resource toca. Um conjunto de fixtures reutilizável para todas as tabelas wp_* vem no PR de infraestrutura de testes.

WpPostResource serialized the whole author model into the Inertia payload.
The author relation selects user_email (needed by WpAuthorService::getAvatar
for the Gravatar hash), so every author address was shipped to the browser on
every post of every listing.

Replace it with the three fields the frontend declares in PostAuthor, and add
$hidden on WpUser so an accidental full-model serialization cannot leak
credentials again. Attribute access is unaffected, so getAvatar still works.

phpunit.xml now pins sqlite/:memory: so the suite can boot at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant