test: add functional regression tests for IDOR fixes (Phase 4)
- Add @japa/api-client devDependency (was missing despite being required
for the 'client' fixture used in functional tests)
- Wire up sessionApiClient/authApiClient/shieldApiClient plugins in
tests/bootstrap.ts so tests can authenticate (loginAs) and pass CSRF
validation (withCsrfToken)
- Wrap each functional test in a global DB transaction (withGlobalTransaction)
for isolation and rollback
- Add .env.test with SESSION_DRIVER=memory: the cookie session driver used
in dev/prod is incompatible with Japa's session test client, which needs
a store it can read/write by session id
- tests/functional/computers.spec.ts: covers owner can update; a user
cannot update/delete another user's computer (404); a user cannot
attach another user's component via componentsId (404); two components
of the same unique type cannot be attached (422); deleting a computer
detaches its components instead of destroying them
- tests/functional/components.spec.ts: covers owner can update; a user
cannot update/delete another user's component (404); an unauthenticated
request cannot delete a component (redirects to /auth/login)
All 10 tests pass, exercising the security fixes from commit 3cc189c.
This commit is contained in:
1 parent
94f89be605
commit
562cea39de
6 files changed
+515
-3
No files matched your search
Vendored
+1
-1
@@ -34,9 +34,9 @@ declare module '@adonisjs/inertia/types' {
|
||||
'widgets/ComputersViewer': ExtractProps<(typeof import('../../inertia/pages/widgets/ComputersViewer.vue'))['default']>
|
||||
'widgets/CustomSelect': ExtractProps<(typeof import('../../inertia/pages/widgets/CustomSelect.vue'))['default']>
|
||||
'widgets/DeleteConfirmationModal': ExtractProps<(typeof import('../../inertia/pages/widgets/DeleteConfirmationModal.vue'))['default']>
|
||||
'widgets/FilterDropdown': ExtractProps<(typeof import('../../inertia/pages/widgets/FilterDropdown.vue'))['default']>
|
||||
'widgets/SimpleGallery': ExtractProps<(typeof import('../../inertia/pages/widgets/SimpleGallery.vue'))['default']>
|
||||
'widgets/TableComponent': ExtractProps<(typeof import('../../inertia/pages/widgets/TableComponent.vue'))['default']>
|
||||
'widgets/ValidationError': ExtractProps<(typeof import('../../inertia/pages/widgets/ValidationError.vue'))['default']>
|
||||
'widgets/FilterDropdown': ExtractProps<(typeof import('../../inertia/pages/widgets/FilterDropdown.vue'))['default']>
|
||||
}
|
||||
}
|
||||
Generated
+194
@@ -39,6 +39,7 @@
|
||||
"@adonisjs/eslint-config": "^3.1.0",
|
||||
"@adonisjs/prettier-config": "^1.5.0",
|
||||
"@adonisjs/tsconfig": "^2.0.0",
|
||||
"@japa/api-client": "^3.2.1",
|
||||
"@japa/assert": "^4.2.0",
|
||||
"@japa/browser-client": "^2.3.0",
|
||||
"@japa/plugin-adonisjs": "^5.2.0",
|
||||
@@ -1864,6 +1865,45 @@
|
||||
"vue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@japa/api-client": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@japa/api-client/-/api-client-3.2.1.tgz",
|
||||
"integrity": "sha512-dICbeEkgGRpjkm3CviOpvPfYMBZddaoW2w20pgNMm3CfvD2bixWOFn6FBRZRq5L+fHYe/O/xfSNGMCQBFy7Zlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@poppinss/hooks": "^7.3.0",
|
||||
"@poppinss/macroable": "^1.1.0",
|
||||
"@poppinss/qs": "^6.15.0",
|
||||
"@types/superagent": "^8.1.9",
|
||||
"cookie-es": "^2.0.0",
|
||||
"set-cookie-parser": "^2.7.2",
|
||||
"superagent": "^10.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.16.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@japa/assert": "^2.0.0 || ^3.0.0 || ^4.0.0",
|
||||
"@japa/openapi-assertions": "^0.1.1",
|
||||
"@japa/runner": "^3.1.2 || ^4.0.0 || ^5.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@japa/assert": {
|
||||
"optional": true
|
||||
},
|
||||
"@japa/openapi-assertions": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@japa/api-client/node_modules/cookie-es": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-2.0.1.tgz",
|
||||
"integrity": "sha512-aVf4A4hI2w70LnF7GG+7xDQUkliwiXWXFvTjkip4+b64ygDQ2sJPRSKFDHbxn8o0xu9QzPkMuuiWIXyFSE2slA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@japa/assert": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@japa/assert/-/assert-4.2.0.tgz",
|
||||
@@ -2058,6 +2098,19 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -2096,6 +2149,16 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@paralleldrive/cuid2": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz",
|
||||
"integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "^1.1.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@phc/format": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz",
|
||||
@@ -3101,6 +3164,13 @@
|
||||
"assertion-error": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cookiejar": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz",
|
||||
"integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/deep-eql": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
||||
@@ -3157,6 +3227,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/methods": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
|
||||
"integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "25.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
|
||||
@@ -3180,6 +3257,19 @@
|
||||
"integrity": "sha512-JOqsl+ZoCpP4e8TDke9W79FDcSgPAR0l6pixx2JHkhnRjvShyYiAYw2LVsnA7K08Y6DeOnaU6ujmENO4os/cYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/superagent": {
|
||||
"version": "8.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.11.tgz",
|
||||
"integrity": "sha512-KA7srSW/HENDtOw9DOqaFLgWuMqN9WgjEw62lh9dpvRaZDkhdOkazASd7X7i2eMUYLHa1U37ZttnePsH5zTDHw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/cookiejar": "^2.1.5",
|
||||
"@types/methods": "^1.1.4",
|
||||
"@types/node": "*",
|
||||
"form-data": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
||||
@@ -3853,6 +3943,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asap": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
|
||||
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/assertion-error": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||
@@ -4528,6 +4625,16 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/component-emitter": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
|
||||
"integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
|
||||
@@ -4560,6 +4667,13 @@
|
||||
"integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookiejar": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
|
||||
"integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/core-js-compat": {
|
||||
"version": "3.49.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz",
|
||||
@@ -4809,6 +4923,17 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dezalgo": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
|
||||
"integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"asap": "^2.0.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/didyoumean": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||
@@ -5797,6 +5922,24 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/formidable": {
|
||||
"version": "3.5.4",
|
||||
"resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz",
|
||||
"integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"dezalgo": "^1.0.4",
|
||||
"once": "^1.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://ko-fi.com/tunnckoCore/commissions"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@@ -7019,6 +7162,16 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/methods": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
@@ -7044,6 +7197,19 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/mime": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
|
||||
"integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mime": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.54.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
||||
@@ -8628,6 +8794,13 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.2",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
||||
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
@@ -9127,6 +9300,27 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/superagent": {
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz",
|
||||
"integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"component-emitter": "^1.3.1",
|
||||
"cookiejar": "^2.1.4",
|
||||
"debug": "^4.3.7",
|
||||
"fast-safe-stringify": "^2.1.1",
|
||||
"form-data": "^4.0.5",
|
||||
"formidable": "^3.5.4",
|
||||
"methods": "^1.1.2",
|
||||
"mime": "2.6.0",
|
||||
"qs": "^6.14.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "10.2.2",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz",
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"@adonisjs/eslint-config": "^3.1.0",
|
||||
"@adonisjs/prettier-config": "^1.5.0",
|
||||
"@adonisjs/tsconfig": "^2.0.0",
|
||||
"@japa/api-client": "^3.2.1",
|
||||
"@japa/assert": "^4.2.0",
|
||||
"@japa/browser-client": "^2.3.0",
|
||||
"@japa/plugin-adonisjs": "^5.2.0",
|
||||
|
||||
+17
-2
@@ -1,9 +1,13 @@
|
||||
import { assert } from '@japa/assert'
|
||||
import { apiClient } from '@japa/api-client'
|
||||
import app from '@adonisjs/core/services/app'
|
||||
import type { Config } from '@japa/runner/types'
|
||||
import { pluginAdonisJS } from '@japa/plugin-adonisjs'
|
||||
import { dbAssertions } from '@adonisjs/lucid/plugins/db'
|
||||
import testUtils from '@adonisjs/core/services/test_utils'
|
||||
import { authApiClient } from '@adonisjs/auth/plugins/api_client'
|
||||
import { sessionApiClient } from '@adonisjs/session/plugins/api_client'
|
||||
import { shieldApiClient } from '@adonisjs/shield/plugins/api_client'
|
||||
|
||||
/**
|
||||
* This file is imported by the "bin/test.ts" entrypoint file
|
||||
@@ -13,7 +17,15 @@ import testUtils from '@adonisjs/core/services/test_utils'
|
||||
* Configure Japa plugins in the plugins array.
|
||||
* Learn more - https://japa.dev/docs/runner-config#plugins-optional
|
||||
*/
|
||||
export const plugins: Config['plugins'] = [assert(), pluginAdonisJS(app), dbAssertions(app)]
|
||||
export const plugins: Config['plugins'] = [
|
||||
assert(),
|
||||
apiClient(),
|
||||
sessionApiClient(app),
|
||||
authApiClient(app),
|
||||
shieldApiClient(),
|
||||
pluginAdonisJS(app),
|
||||
dbAssertions(app),
|
||||
]
|
||||
|
||||
/**
|
||||
* Configure lifecycle function to run before and after all the
|
||||
@@ -33,6 +45,9 @@ export const runnerHooks: Required<Pick<Config, 'setup' | 'teardown'>> = {
|
||||
*/
|
||||
export const configureSuite: Config['configureSuite'] = (suite) => {
|
||||
if (['browser', 'functional', 'e2e'].includes(suite.name)) {
|
||||
return suite.setup(() => testUtils.httpServer().start())
|
||||
suite.setup(() => testUtils.httpServer().start())
|
||||
suite.onGroup((group) => {
|
||||
group.each.setup(() => testUtils.db().withGlobalTransaction())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { test } from '@japa/runner'
|
||||
import { DateTime } from 'luxon'
|
||||
import User from '#models/user'
|
||||
import Component from '#models/component'
|
||||
import ComponentType from '#models/component_type'
|
||||
|
||||
async function createUser(email: string) {
|
||||
return User.create({
|
||||
name: 'Test User',
|
||||
email,
|
||||
password: 'secret@123A',
|
||||
})
|
||||
}
|
||||
|
||||
async function createType(name: string, unique = true) {
|
||||
return ComponentType.firstOrCreate({ name }, { name, unique })
|
||||
}
|
||||
|
||||
async function createComponent(userId: number, typeId: number) {
|
||||
return Component.create({
|
||||
name: 'Test Component',
|
||||
price: 20,
|
||||
typeId,
|
||||
userId,
|
||||
computerId: null,
|
||||
purchaseDate: DateTime.now(),
|
||||
})
|
||||
}
|
||||
|
||||
test.group('Components authorization (IDOR)', () => {
|
||||
test('owner can update their own component', async ({ client, assert }) => {
|
||||
const owner = await createUser('owner-comp-update@example.com')
|
||||
const type = await createType('RAM-comp-1')
|
||||
const component = await createComponent(owner.id, type.id)
|
||||
|
||||
const response = await client
|
||||
.put(`/components/${component.id}`)
|
||||
.loginAs(owner)
|
||||
.withCsrfToken()
|
||||
.redirects(0)
|
||||
.json({
|
||||
name: 'Updated name',
|
||||
price: 30,
|
||||
typeId: type.id,
|
||||
purchaseDate: DateTime.now().toFormat('yyyy-MM-dd'),
|
||||
})
|
||||
|
||||
response.assertStatus(302)
|
||||
await component.refresh()
|
||||
assert.equal(component.name, 'Updated name')
|
||||
})
|
||||
|
||||
test('a user cannot update another user component', async ({ client, assert }) => {
|
||||
const owner = await createUser('owner-comp-a@example.com')
|
||||
const attacker = await createUser('attacker-comp-a@example.com')
|
||||
const type = await createType('RAM-comp-2')
|
||||
const component = await createComponent(owner.id, type.id)
|
||||
|
||||
const response = await client
|
||||
.put(`/components/${component.id}`)
|
||||
.loginAs(attacker)
|
||||
.withCsrfToken()
|
||||
.redirects(0)
|
||||
.json({
|
||||
name: 'Hacked name',
|
||||
price: 1,
|
||||
typeId: type.id,
|
||||
purchaseDate: DateTime.now().toFormat('yyyy-MM-dd'),
|
||||
})
|
||||
|
||||
response.assertStatus(404)
|
||||
await component.refresh()
|
||||
assert.notEqual(component.name, 'Hacked name')
|
||||
})
|
||||
|
||||
test('a user cannot delete another user component', async ({ client, assert }) => {
|
||||
const owner = await createUser('owner-comp-b@example.com')
|
||||
const attacker = await createUser('attacker-comp-b@example.com')
|
||||
const type = await createType('RAM-comp-3')
|
||||
const component = await createComponent(owner.id, type.id)
|
||||
|
||||
const response = await client
|
||||
.delete(`/components/${component.id}`)
|
||||
.loginAs(attacker)
|
||||
.withCsrfToken()
|
||||
.redirects(0)
|
||||
|
||||
response.assertStatus(404)
|
||||
const stillExists = await Component.find(component.id)
|
||||
assert.isNotNull(stillExists)
|
||||
})
|
||||
|
||||
test('an unauthenticated request cannot delete a component', async ({ client, assert }) => {
|
||||
const owner = await createUser('owner-comp-c@example.com')
|
||||
const type = await createType('RAM-comp-4')
|
||||
const component = await createComponent(owner.id, type.id)
|
||||
|
||||
const response = await client.delete(`/components/${component.id}`)
|
||||
|
||||
response.assertRedirectsTo('/auth/login')
|
||||
const stillExists = await Component.find(component.id)
|
||||
assert.isNotNull(stillExists)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,198 @@
|
||||
import { test } from '@japa/runner'
|
||||
import { DateTime } from 'luxon'
|
||||
import User from '#models/user'
|
||||
import Computer from '#models/computer'
|
||||
import ComputerState from '#models/computer_state'
|
||||
import Component from '#models/component'
|
||||
import ComponentType from '#models/component_type'
|
||||
|
||||
async function createUser(email: string) {
|
||||
return User.create({
|
||||
name: 'Test User',
|
||||
email,
|
||||
password: 'secret@123A',
|
||||
})
|
||||
}
|
||||
|
||||
async function createState(name = 'ON_GOING') {
|
||||
return ComputerState.firstOrCreate({ name }, { name })
|
||||
}
|
||||
|
||||
async function createType(name: string, unique = true) {
|
||||
return ComponentType.firstOrCreate({ name }, { name, unique })
|
||||
}
|
||||
|
||||
async function createComponent(userId: number, typeId: number, computerId: number | null = null) {
|
||||
return Component.create({
|
||||
name: 'Test Component',
|
||||
price: 42.5,
|
||||
typeId,
|
||||
userId,
|
||||
computerId,
|
||||
purchaseDate: DateTime.now(),
|
||||
})
|
||||
}
|
||||
|
||||
async function createComputer(userId: number, stateId: number) {
|
||||
return Computer.create({
|
||||
title: 'My PC',
|
||||
sellPrice: 100,
|
||||
gpuScore: 1,
|
||||
cpuScore: 1,
|
||||
globalScore: 1,
|
||||
stateId,
|
||||
userId,
|
||||
})
|
||||
}
|
||||
|
||||
test.group('Computers authorization (IDOR)', () => {
|
||||
test('owner can update their own computer', async ({ client, assert }) => {
|
||||
const owner = await createUser('owner-update@example.com')
|
||||
const state = await createState()
|
||||
const computer = await createComputer(owner.id, state.id)
|
||||
|
||||
const response = await client
|
||||
.put(`/computers/${computer.id}`)
|
||||
.loginAs(owner)
|
||||
.withCsrfToken()
|
||||
.redirects(0)
|
||||
.json({
|
||||
title: 'Updated title',
|
||||
sellPrice: 150,
|
||||
gpuScore: 2,
|
||||
cpuScore: 2,
|
||||
globalScore: 2,
|
||||
stateId: state.id,
|
||||
componentsId: [],
|
||||
pictures: [],
|
||||
})
|
||||
|
||||
response.assertStatus(302)
|
||||
await computer.refresh()
|
||||
assert.equal(computer.title, 'Updated title')
|
||||
})
|
||||
|
||||
test('a user cannot update another user computer', async ({ client }) => {
|
||||
const owner = await createUser('owner-a@example.com')
|
||||
const attacker = await createUser('attacker-a@example.com')
|
||||
const state = await createState()
|
||||
const computer = await createComputer(owner.id, state.id)
|
||||
|
||||
const response = await client
|
||||
.put(`/computers/${computer.id}`)
|
||||
.loginAs(attacker)
|
||||
.withCsrfToken()
|
||||
.redirects(0)
|
||||
.json({
|
||||
title: 'Hacked title',
|
||||
sellPrice: 1,
|
||||
gpuScore: 1,
|
||||
cpuScore: 1,
|
||||
globalScore: 1,
|
||||
stateId: state.id,
|
||||
componentsId: [],
|
||||
pictures: [],
|
||||
})
|
||||
|
||||
response.assertStatus(404)
|
||||
})
|
||||
|
||||
test('a user cannot delete another user computer', async ({ client, assert }) => {
|
||||
const owner = await createUser('owner-b@example.com')
|
||||
const attacker = await createUser('attacker-b@example.com')
|
||||
const state = await createState()
|
||||
const computer = await createComputer(owner.id, state.id)
|
||||
|
||||
const response = await client
|
||||
.delete(`/computers/${computer.id}`)
|
||||
.loginAs(attacker)
|
||||
.withCsrfToken()
|
||||
.redirects(0)
|
||||
|
||||
response.assertStatus(404)
|
||||
const stillExists = await Computer.find(computer.id)
|
||||
assert.isNotNull(stillExists)
|
||||
})
|
||||
|
||||
test('a user cannot attach another user component via componentsId', async ({
|
||||
client,
|
||||
assert,
|
||||
}) => {
|
||||
const owner = await createUser('owner-c@example.com')
|
||||
const attacker = await createUser('attacker-c@example.com')
|
||||
const state = await createState()
|
||||
const type = await createType('Processor-IDOR', true)
|
||||
const victimComponent = await createComponent(owner.id, type.id)
|
||||
const attackerComputer = await createComputer(attacker.id, state.id)
|
||||
|
||||
const response = await client
|
||||
.put(`/computers/${attackerComputer.id}`)
|
||||
.loginAs(attacker)
|
||||
.withCsrfToken()
|
||||
.redirects(0)
|
||||
.json({
|
||||
title: attackerComputer.title,
|
||||
sellPrice: attackerComputer.sellPrice,
|
||||
gpuScore: 0,
|
||||
cpuScore: 0,
|
||||
globalScore: 0,
|
||||
stateId: state.id,
|
||||
componentsId: [victimComponent.id],
|
||||
pictures: [],
|
||||
})
|
||||
|
||||
// handleComponents scopes the lookup to the current user, so the
|
||||
// victim's component cannot be found/attached -> 404.
|
||||
response.assertStatus(404)
|
||||
await victimComponent.refresh()
|
||||
assert.isNull(victimComponent.computerId)
|
||||
})
|
||||
|
||||
test('cannot attach two components of the same unique type to a computer', async ({ client }) => {
|
||||
const owner = await createUser('owner-d@example.com')
|
||||
const state = await createState()
|
||||
const type = await createType('Motherboard-IDOR', true)
|
||||
const componentA = await createComponent(owner.id, type.id)
|
||||
const componentB = await createComponent(owner.id, type.id)
|
||||
const computer = await createComputer(owner.id, state.id)
|
||||
|
||||
const response = await client
|
||||
.put(`/computers/${computer.id}`)
|
||||
.loginAs(owner)
|
||||
.withCsrfToken()
|
||||
.redirects(0)
|
||||
.json({
|
||||
title: computer.title,
|
||||
sellPrice: computer.sellPrice,
|
||||
gpuScore: 0,
|
||||
cpuScore: 0,
|
||||
globalScore: 0,
|
||||
stateId: state.id,
|
||||
componentsId: [componentA.id, componentB.id],
|
||||
pictures: [],
|
||||
})
|
||||
|
||||
response.assertStatus(422)
|
||||
})
|
||||
|
||||
test('deleting a computer detaches its components instead of destroying them', async ({
|
||||
client,
|
||||
assert,
|
||||
}) => {
|
||||
const owner = await createUser('owner-e@example.com')
|
||||
const state = await createState()
|
||||
const type = await createType('Fan-IDOR', false)
|
||||
const computer = await createComputer(owner.id, state.id)
|
||||
const component = await createComponent(owner.id, type.id, computer.id)
|
||||
|
||||
const response = await client
|
||||
.delete(`/computers/${computer.id}`)
|
||||
.loginAs(owner)
|
||||
.withCsrfToken()
|
||||
.redirects(0)
|
||||
|
||||
response.assertStatus(302)
|
||||
await component.refresh()
|
||||
assert.isNull(component.computerId)
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user