From b36232d87a645b5a338dbb3b7de91537fdc55e74 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Wed, 22 Jul 2026 11:29:33 +0100 Subject: [PATCH] Validate snapshot ownership against device group team --- forge/ee/db/controllers/DeviceGroup.js | 7 +++++ .../api/applicationDeviceGroups_spec.js | 31 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/forge/ee/db/controllers/DeviceGroup.js b/forge/ee/db/controllers/DeviceGroup.js index 11bb899eeb..dc55c5b52e 100644 --- a/forge/ee/db/controllers/DeviceGroup.js +++ b/forge/ee/db/controllers/DeviceGroup.js @@ -104,6 +104,13 @@ module.exports = { if (!snapshot) { throw new ValidationError('Snapshot does not exist') } + // ensure the snapshot belongs to the same team as the device group's + // application - never trust a payload-supplied snapshot id to be in-team + const snapshotTeamId = await snapshot.getTeamId() + const application = await deviceGroup.getApplication() + if (!snapshotTeamId || !application || snapshotTeamId !== application.TeamId) { + throw new ValidationError('Snapshot does not belong to the same team') + } snapshotId = snapshot.id } diff --git a/test/unit/forge/ee/routes/api/applicationDeviceGroups_spec.js b/test/unit/forge/ee/routes/api/applicationDeviceGroups_spec.js index ed0638f257..a8c1c787d2 100644 --- a/test/unit/forge/ee/routes/api/applicationDeviceGroups_spec.js +++ b/test/unit/forge/ee/routes/api/applicationDeviceGroups_spec.js @@ -529,6 +529,37 @@ describe('Application Device Groups API', function () { app.comms.devices.sendCommand.callCount.should.equal(0) // no devices should have been sent an update }) + // Bob (BTeam owner) should not be able to point his BTeam device group at an ATeam snapshot. + it('Rejects a targetSnapshot that belongs to another team (400)', async function () { + const { sid, application, deviceGroup, device1of2, device2of2, snapshot } = await prepare() + + // Create an application, instance and snapshot owned by ATeam (foreign to bob's BTeam group) + const foreignApplication = await factory.createApplication({ name: generateName('foreign-app') }, TestObjects.ATeam) + const foreignInstance = await factory.createInstance({ name: generateName('foreign-instance') }, foreignApplication, app.stack, app.template, app.projectType, { start: false }) + const foreignSnapshot = await factory.createSnapshot({ name: generateName('foreign-snapshot') }, foreignInstance, TestObjects.alice) + + // now call the API to point the group at the foreign snapshot + const response = await callUpdate(sid, application, deviceGroup, { + targetSnapshotId: foreignSnapshot.hashid + }) + + // should fail + response.statusCode.should.equal(400) + response.json().should.have.property('code', 'invalid_input') + + const updatedDeviceGroup = await app.db.models.DeviceGroup.byId(deviceGroup.hashid) + const updatedDevice1 = await app.db.models.Device.byId(device1of2.hashid) + const updatedDevice2 = await app.db.models.Device.byId(device2of2.hashid) + + // should not have updated the group or devices - still the original in-team snapshot + updatedDeviceGroup.should.have.property('targetSnapshotId', snapshot.id) + updatedDevice1.should.have.property('targetSnapshotId', snapshot.id) + updatedDevice2.should.have.property('targetSnapshotId', snapshot.id) + + // check no devices got an update command + app.comms.devices.sendCommand.callCount.should.equal(0) + }) + it('Cannot update a device group with empty name', async function () { const sid = await login('bob', 'bbPassword') const application = await factory.createApplication({ name: generateName('app') }, TestObjects.BTeam)