Skip to content

bug: require login, ownership and CSRF token on todo routes (sfind-ff2cbbbc) - #1104

Open
devin-ai-integration[bot] wants to merge 2 commits into
mainfrom
devin/sfind-ff2cbbbc-todo-routes-auth-ownership
Open

devin-ai-integration[bot] wants to merge 2 commits into
mainfrom
devin/sfind-ff2cbbbc-todo-routes-auth-ownership

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Resumo

Corrige a falta de autorização nas rotas de todo (finding sfind-ff2cbbbc, severidade média, routes/index.js:152,190,191,203,218,219,241).

Antes, app.js registrava /create, /destroy/:id, /edit/:id, /update/:id e /import sem routes.isLoggedIn. destroy e update carregavam qualquer todo pelo id da URL, sem checar o dono. /destroy/:id era um GET, então uma tag <img> em outro site já apagava um todo. Um visitante anônimo podia apagar ou reescrever todos os todos.

Mudanças:

  • app.js: as 5 rotas passam por routes.isLoggedIn; create/destroy/update/import também passam por routes.verifyCsrf. /destroy/:id agora é só POST (GET dá 404).
  • mongoose-db.js: Todo ganha o campo owner (String).
  • routes/index.js:
    • o login grava session.username;
    • create e import gravam owner;
    • edit/update/destroy usam findOwnedTodo (Todo.findOne({ _id, owner })) e devolvem 404 se o todo não é do usuário, se o id é inválido ou se a sessão não tem username;
    • verifyCsrf compara _csrf (body) ou X-CSRF-Token com o token da sessão via crypto.timingSafeEqual; sem token ou com token errado dá 403;
    • current_user cria o token e expõe csrfToken / currentUser em res.locals.
  • Views: os forms de create, update e import levam _csrf hidden. O delete virou um form POST com botão. Editar e apagar só aparecem nos todos do próprio usuário. Visitante anônimo vê a lista, mais um link "Log in". screen.css estiliza o botão de delete como o link antigo.
  • tests/todo-authz.test.js (node:test, sem dependências novas): 11 casos (inclui token CSRF não-ASCII e sessão anônima sem token).

Mudanças de contrato da API:

  • GET /destroy/:id foi removido. Agora é POST /destroy/:id com _csrf.
  • POST /create, /update/:id e /import exigem sessão logada e _csrf.
  • Todos antigos, sem owner, ficam só leitura.
  • Os scripts em exploits/ (ms-exploits.sh, mongoose-exploits.sh, moment-exploit.sh) chamam /create e /import sem sessão. Agora eles recebem redirect para /. Para reproduzir esses demos, é preciso enviar cookie de sessão e _csrf.

Como testar

  • NODE_OPTIONS=--openssl-legacy-provider node --test tests/todo-authz.test.js: 11/11 neste branch. Em main falha, porque verifyCsrf não existe e destroy/update não checam o dono.
  • E2E com o app.js real (node -r memdb.js app.js, um Mongo em memória, porque o mongod 7 local não é compatível com mongoose 4.2.4). alice = [REDACTED SECRET]@snyk.io, bob = segundo usuário:
requisição main este PR
anônimo GET /destroy/<id da alice> 302, todo apagado 404, nada apagado
anônimo POST /destroy/<id> 404 (rota não existe) 302 para /, nada apagado
anônimo POST /update/<id> 302, conteúdo reescrito 302 para /, sem mudança
anônimo POST /create / POST /import 302, todos criados 302 para /, nada criado
anônimo GET /edit/<id> 200 302 para /
bob POST /update / POST /destroy / GET /edit no todo da alice 302 / - / 200 404 / 404 / 404
alice POST /destroy sem _csrf ou com _csrf errado - 403
alice edit / update / import / destroy do próprio todo, com _csrf - 200 / 302 / 302 / 302 (funciona)

npm test (= snyk test) não roda aqui: precisa de autenticação/rede no Snyk.

Devin-Org: engineering

Link to Devin session: https://devin-gtm.devinenterprise.com/sessions/75569c389ead44e0b4e2b3b54e482e59
Open in Devin Desktop: https://devin-gtm.devinenterprise.com/desktop/session/75569c389ead44e0b4e2b3b54e482e59?variant=devin


Devin Review

Status Commit
⚪ Not started —

Run Devin Review

Enable auto-review to review your pull requests automatically.

Devin Review (Staging)


Devin Review

…ate/destroy/import

Todo routes were registered without isLoggedIn and destroy/update loaded
any todo by URL id, so anonymous visitors could rewrite or delete every
todo (destroy was a GET, so an <img> could trigger it).

- create/edit/update/destroy/import now go through isLoggedIn
- destroy is POST-only; mutating routes check a per-session CSRF token
- todos store an owner (session username, set at login) and
  edit/update/destroy only match the caller's own todos (404 otherwise)
- views show edit/delete controls only for the owner's todos

Refs: sfind-ff2cbbbc371c404c904ae667c8abf99c
@devin-ai-integration

Copy link
Copy Markdown
Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

github-advanced-security[bot]

This comment was marked as resolved.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

Comment thread routes/index.js Resolved
Comment thread routes/index.js
Comment on lines +117 to +119
function currentUsername(req) {
var username = req.session && req.session.username;
return req.session && req.session.loggedIn === 1 && typeof username === 'string' && username ? username : null;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Existing sessions lose todo controls

After deployment, currentUsername rejects existing logged-in sessions without a username. isLoggedIn still accepts them, so todo controls disappear and edits return 404 until the user logs in again.

Learn more

Sessions created before this change have loggedIn but no username, because login did not previously store it. isLoggedIn accepts these sessions, while the new username check fails. The index then hides the create and ownership controls, and the ownership lookup returns no todo. An API client that submits a valid token to create/import can also persist a todo with owner: null.

Example: Alice logs in before deployment and keeps her session cookie. After deployment, she visits / and sees a login prompt instead of her todo controls. /edit/<her-id> returns 404 until she signs in again.

Recommended fix: Handle the missing username as an expired session and require reauthentication before allowing todo writes; alternatively migrate sessions safely. Ensure the write routes reject a missing owner rather than saving null.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread routes/index.js Resolved
Comment thread routes/index.js
Comment on lines +223 to +231
if (err) return next(err);
res.redirect('/');
});
});
};

exports.edit = function (req, res, next) {
Todo.
find({}).
sort('-updated_at').
exec(function (err, todos) {
if (err) return next(err);
findOwnedTodo(req, function (err, owned) {
if (err) return next(err);
@devin-ai-integration

Copy link
Copy Markdown
Author

Validação no navegador — 46f5e1d

Rodei o app real localmente com um stand-in de mongoose em memória. Persistência com MongoDB real não foi validada: o mongod 7 local não fala com o mongoose 4.2.4 e o download de uma versão compatível foi bloqueado pela rede.

  • ✅ Usuário A criou, editou e apagou seu todo pelo botão POST.
  • ✅ Usuário B viu o todo de A somente para leitura; acesso direto a /edit/<id> retornou 404.
  • ✅ Anônimo viu o link de login, sem formulários de criação/importação nem controles de edição/exclusão.
  • ✅ GET /destroy/<id> retornou 404 e manteve o todo.
  • ✅ Importação criou um todo com dono e controles de edição/exclusão.
  • ✅ Formulário de criação sem _csrf retornou 403 e não criou o item.
  • ✅ Botão de exclusão tem a mesma aparência do link antigo (ícone 16×18 idêntico).
Dono: controles disponíveis Outro usuário: somente leitura
Dono Outro usuário

Gravação do fluxo

Comparação visual do controle de exclusão (antes / depois)

Antes e depois

Written by Devin

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