fix(security): close IDOR on computers/components update & destroy

- Scope update/destroy queries by userId (Computers & Components), matching
  the pattern already used in index/show/edit
- Add missing auth.authenticate() calls on destroy actions
- handleComponents now requires userId and scopes component lookups to the
  current user, preventing cross-user component takeover
- handleComponents rejects attaching more than one component of the same
  'unique' type to a computer (new InvalidComponentSelectionException, 422)
- handlePictures scopes picture lookups by computerId
- destroy(Computer) now detaches its components (computerId = null) instead
  of losing them to cascade delete, and removes the computer's uploads dir
- Switch store/update actions to request.validateUsing() for consistency
- Fix relative redirects to absolute paths (/computers, /computers/:id)
- Untrack .env.production and add it to .gitignore (secrets must not be
  committed); local file kept on disk
This commit is contained in:
Kevin L'Hereec committed 2026-07-23 10:53:13 +02:00
1 parent ec6734f491
commit 3cc189c534
7 files changed
+417 -24

No files matched your search

+27 -12
View File
@@ -1,4 +1,5 @@
import type { HttpContext } from '@adonisjs/core/http'
import { existsSync, rmSync } from 'node:fs'
import Computer from '#models/computer'
import ComputerState from '#models/computer_state'
import Component from '#models/component'
@@ -33,17 +34,17 @@ export default class ComputersController {
}
async store({ auth, request, response }: HttpContext) {
const data = request.all()
const user = await auth.authenticate()
const { pictures, componentsId, ...computerData } = await storeComputerValidator.validate(data)
const { pictures, componentsId, ...computerData } =
await request.validateUsing(storeComputerValidator)
await ComputerState.findOrFail(computerData.stateId)
const computer = await Computer.create({
...computerData,
userId: user.id,
})
await handleComponents(componentsId as number[], computer)
await handleComponents(componentsId as number[], computer, user.id)
await handlePictures(pictures as Image[], request, computer)
return response.redirect('computers/')
return response.redirect('/computers')
}
async show({ auth, inertia, params }: HttpContext) {
@@ -89,22 +90,36 @@ export default class ComputersController {
return inertia.render('computers/edit', { computer: computer.serialize(), availableComponents: availableComponents.map((c) => c.serialize()), states: states.map((s) => s.serialize()) })
}
async update({ params, request, response }: HttpContext) {
async update({ auth, params, request, response }: HttpContext) {
const user = await auth.authenticate()
const computerId = params.id
const { pictures, componentsId, ...computerData } = await updateComputerValidator.validate(
request.all()
const { pictures, componentsId, ...computerData } = await request.validateUsing(
updateComputerValidator
)
const computer = await Computer.findOrFail(computerId)
const computer = await Computer.query()
.where('user_id', user.id)
.andWhere('id', computerId)
.firstOrFail()
computer.merge(computerData)
await computer.save()
await handleComponents(componentsId as number[], computer)
await handleComponents(componentsId as number[], computer, user.id)
await handlePictures(pictures as Image[], request, computer)
return response.redirect(`${computer.id}`)
return response.redirect(`/computers/${computer.id}`)
}
async destroy({ params, response }: HttpContext) {
async destroy({ auth, params, response }: HttpContext) {
const user = await auth.authenticate()
const computerId = params.id
const computer = await Computer.findOrFail(computerId)
const computer = await Computer.query()
.where('user_id', user.id)
.andWhere('id', computerId)
.firstOrFail()
const uploadsDir = `uploads/computer_${computer.id}`
await computer.related('components').query().update({ computerId: null })
if (existsSync(uploadsDir)) {
rmSync(uploadsDir, { recursive: true, force: true })
}
await computer.delete()
return response.redirect('/computers')
}