Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## [0.11.14] - 2026-08-24

### Security

- **Cible composant déplacée dans l'URL** : `PUT`/`DELETE /workspaces/:id/components/:componentId`
(au lieu de `body._id`) — la cible d'écriture vient des params, le body ne porte que le
contenu. Élimine la dépendance à `body._id` comme cible non contrôlée.
- `PUT /workspaces/:id` et `bigdataflow` : objet intermédiaire `{...body, _id: params.id}`
(le body d'origine n'est plus muté).

## [0.11.13] - 2026-08-24

### Security
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "semantic-bus-monorepo",
"version": "0.11.13",
"version": "0.11.14",
"description": "ETL style data middleware transformation embedded in an ESB for all kind of data",
"private": true,
"workspaces": [
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@semantic-bus/core",
"version": "0.11.13",
"version": "0.11.14",
"description": "Core module for Semantic Bus",
"private": true,
"main": "index.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/engine/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@semantic-bus/engine",
"version": "0.11.13",
"version": "0.11.14",
"description": "Processing engine module for Semantic Bus",
"private": true,
"main": "app.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/eval-service/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@semantic-bus/eval-service",
"version": "0.11.13",
"version": "0.11.14",
"description": "Service d'évaluation JavaScript isolé en container (dédié aux eval des transformations / $where).",
"private": true,
"main": "app.js",
Expand Down
27 changes: 14 additions & 13 deletions packages/main/__tests__/server/workspaceSecurity.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,35 +112,36 @@ describe('workspaceWebService - confused deputy sur les routes sœurs (SB-IDOR-2
const res = { send: jest.fn() };
const next = jest.fn();
await mw[1](req, res, next);
// le body._id doit être écrasé par req.params.id (pas de confused deputy)
expect(workspaceLib.update).toHaveBeenCalledWith(expect.objectContaining({ _id: 'WS_AUTHORIZED' }));
expect(req.body._id).toBe('WS_AUTHORIZED');
// la cible vient de req.params.id via un objet intermédiaire — req.body n est pas muté
expect(workspaceLib.update).toHaveBeenCalledWith(expect.objectContaining({ _id: 'WS_AUTHORIZED', name: 'PWNED' }));
expect(req.body._id).toBe('WS_ATTACKER'); // le body d'origine n est pas modifié
});

test('PUT /workspaces/:id/components vérifie l appartenance du composant au workspace', async () => {
const mw = routes.store.put['/workspaces/:id/components'];
const req = { params: { id: 'WS_AUTHORIZED' }, body: { _id: 'COMP_X', name: 'x' } };
test('PUT /workspaces/:id/components/:componentId vérifie l appartenance du composant au workspace', async () => {
const mw = routes.store.put['/workspaces/:id/components/:componentId'];
const req = { params: { id: 'WS_AUTHORIZED', componentId: 'COMP_X' }, body: { name: 'x' } };
const res = { json: jest.fn() };
const next = jest.fn();
await mw[1](req, res, next);
expect(workspaceComponentLib.assertComponentInWorkspace).toHaveBeenCalledWith('COMP_X', 'WS_AUTHORIZED');
expect(workspaceComponentLib.update).toHaveBeenCalled();
// la cible (componentId) et le workspaceId viennent des params, jamais du body._id
expect(workspaceComponentLib.update).toHaveBeenCalledWith(expect.objectContaining({ _id: 'COMP_X', workspaceId: 'WS_AUTHORIZED' }));
});

test('DELETE /workspaces/:id/components vérifie l appartenance du composant au workspace', async () => {
const mw = routes.store.delete['/workspaces/:id/components'];
const req = { params: { id: 'WS_AUTHORIZED' }, body: { _id: 'COMP_X' } };
test('DELETE /workspaces/:id/components/:componentId vérifie l appartenance du composant au workspace', async () => {
const mw = routes.store.delete['/workspaces/:id/components/:componentId'];
const req = { params: { id: 'WS_AUTHORIZED', componentId: 'COMP_X' }, body: {} };
const res = { json: jest.fn() };
const next = jest.fn();
await mw[1](req, res, next);
expect(workspaceComponentLib.assertComponentInWorkspace).toHaveBeenCalledWith('COMP_X', 'WS_AUTHORIZED');
expect(workspaceComponentLib.remove).toHaveBeenCalledWith({ _id: 'COMP_X' });
});

test('DELETE /workspaces/:id/components propage l erreur si composant hors workspace', async () => {
const mw = routes.store.delete['/workspaces/:id/components'];
test('DELETE /workspaces/:id/components/:componentId propage l erreur si composant hors workspace', async () => {
const mw = routes.store.delete['/workspaces/:id/components/:componentId'];
workspaceComponentLib.assertComponentInWorkspace.mockRejectedValueOnce(new Error('component_not_in_workspace'));
const req = { params: { id: 'WS_AUTHORIZED' }, body: { _id: 'COMP_X' } };
const req = { params: { id: 'WS_AUTHORIZED', componentId: 'COMP_X' }, body: {} };
const res = { json: jest.fn() };
const next = jest.fn();
await mw[1](req, res, next);
Expand Down
6 changes: 3 additions & 3 deletions packages/main/client/static/store/workspaceStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -602,7 +602,7 @@ function WorkspaceStore(utilStore, specificStoreList) {
return new Promise((resolve, reject) => {
utilStore.ajaxCall({
method: 'put',
url: '../data/core/workspaces/' + this.itemCurrent.workspaceId + '/components',
url: '../data/core/workspaces/' + this.itemCurrent.workspaceId + '/components/' + this.itemCurrent._id,
data: JSON.stringify(this.workspaceBusiness.serialiseWorkspaceComponent(this.itemCurrent))
}, true).then(data => {
this.itemCurrent = data
Expand Down Expand Up @@ -661,7 +661,7 @@ function WorkspaceStore(utilStore, specificStoreList) {
this.on('item_persist', function (item) {
this.utilStore.ajaxCall({
method: 'put',
url: '../data/core/workspaces/' + item.workspaceId + '/components',
url: '../data/core/workspaces/' + item.workspaceId + '/components/' + item._id,
data: JSON.stringify(this.workspaceBusiness.serialiseWorkspaceComponent(item))
}, true).then(data => {
item = data
Expand Down Expand Up @@ -1009,7 +1009,7 @@ function WorkspaceStore(utilStore, specificStoreList) {
this.on('workspace_current_delete_component', function (record) {
this.utilStore.ajaxCall({
method: 'delete',
url: '../data/core/workspaces/' + record.workspaceId + '/components',
url: '../data/core/workspaces/' + record.workspaceId + '/components/' + record._id,
data: JSON.stringify(this.workspaceBusiness.serialiseWorkspaceComponent(record))
}, true).then(data => {
sift({
Expand Down
2 changes: 1 addition & 1 deletion packages/main/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@semantic-bus/main",
"version": "0.11.13",
"version": "0.11.14",
"description": "Main application module for Semantic Bus",
"private": true,
"main": "app.js",
Expand Down
8 changes: 4 additions & 4 deletions packages/main/server/bigdataflowService.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,10 +165,10 @@ module.exports = function (router) {

router.put('/bigdataflow/:id', (req, res, next) => securityService.wrapperSecurity(req, res, next,undefined,'bigdataflow'), (req, res, next)=> {
if (req.body != null) {
// SÉCURITÉ : lier la cible d'écriture au bigdataflow autorisé par wrapperSecurity.
// Ne jamais autoriser sur req.params.id et écrire sur req.body._id (confused deputy).
req.body._id = req.params.id
bigdataflow_lib.update(req.body).then(workspaceUpdated => {
// SÉCURITÉ : la cible d'écriture est le bigdataflow autorisé par wrapperSecurity.
// Objet intermédiaire : le body (contenu) + l'id venant des params (jamais body._id).
const bdUpdate = { ...req.body, _id: req.params.id }
bigdataflow_lib.update(bdUpdate).then(workspaceUpdated => {
res.send(workspaceUpdated)
}).catch(e => {
next(e)
Expand Down
42 changes: 24 additions & 18 deletions packages/main/server/workspaceWebService.js
Original file line number Diff line number Diff line change
Expand Up @@ -362,14 +362,16 @@ module.exports = function (router) {

// --------------------------------------------------------------------------------

router.delete('/workspaces/:id/components', (req, res, next) => securityService.wrapperSecurity(req, res, next,undefined,'workflow'), async function (req, res, next) {
router.delete('/workspaces/:id/components/:componentId', (req, res, next) => securityService.wrapperSecurity(req, res, next,undefined,'workflow'), async function (req, res, next) {
try {
// SÉCURITÉ : le composant cible doit appartenir au workspace autorisé (req.params.id).
await workspace_component_lib.assertComponentInWorkspace(req.body._id, req.params.id)
// SÉCURITÉ : le composant cible vient de l'URL (req.params.componentId) et doit
// appartenir au workspace autorisé (req.params.id). Aucune cible body._id.
const componentId = req.params.componentId
await workspace_component_lib.assertComponentInWorkspace(componentId, req.params.id)
await workspace_component_lib.remove({
_id: req.body._id
_id: componentId
})
res.json(req.body)
res.json({ _id: componentId })
} catch (e) {
if (e && e.status) {
return res.status(e.status).send({ success: false, message: e.message || 'Forbidden' })
Expand All @@ -390,14 +392,18 @@ module.exports = function (router) {

// --------------------------------------------------------------------------------

router.put('/workspaces/:id/components', (req, res, next) => securityService.wrapperSecurity(req, res, next,undefined,'workflow'), async function (req, res, next) {
router.put('/workspaces/:id/components/:componentId', (req, res, next) => securityService.wrapperSecurity(req, res, next,undefined,'workflow'), async function (req, res, next) {
try {
// SÉCURITÉ : le composant cible doit appartenir au workspace autorisé (req.params.id).
if (req.body && req.body._id) {
await workspace_component_lib.assertComponentInWorkspace(req.body._id, req.params.id)
req.body.workspaceId = req.params.id
}
const componentUpdated = await workspace_component_lib.update(req.body)
// SÉCURITÉ : la cible vient de l'URL (req.params.componentId) et doit appartenir
// au workspace autorisé (req.params.id). L'objet à écrire assemble le body (contenu)
// avec l'id et le workspaceId issus des params — jamais body._id.
const componentId = req.params.componentId
await workspace_component_lib.assertComponentInWorkspace(componentId, req.params.id)
const componentUpdated = await workspace_component_lib.update({
...req.body,
_id: componentId,
workspaceId: req.params.id
})
res.json(componentUpdated)
} catch (e) {
if (e && e.status) {
Expand Down Expand Up @@ -486,18 +492,18 @@ module.exports = function (router) {

router.put('/workspaces/:id', (req, res, next) => securityService.wrapperSecurity(req, res, next,undefined,'workflow'), function (req, res, next) {
if (req.body != null) {
// SÉCURITÉ : lier la cible d'écriture au workspace autorisé par wrapperSecurity.
// Ne jamais autoriser sur req.params.id et écrire sur req.body._id (confused deputy).
req.body._id = req.params.id
workspace_lib.update(req.body).then(workspaceUpdate => {
for (var c of workspaceUpdate.components) {
// SÉCURITÉ : la cible d'écriture est le workspace autorisé par wrapperSecurity.
// Objet intermédiaire : le body (contenu) + l'id venant des params (jamais body._id).
const workspaceUpdate = { ...req.body, _id: req.params.id }
workspace_lib.update(workspaceUpdate).then(result => {
for (var c of result.components) {
if (technicalComponentDirectory[c.module] != null) {
c.graphIcon = technicalComponentDirectory[c.module].graphIcon
} else {
c.graphIcon = 'default'
}
}
res.send(workspaceUpdate)
res.send(result)
}).catch(e => {
next(e)
}).catch(e => {
Expand Down
2 changes: 1 addition & 1 deletion packages/timer/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@semantic-bus/timer",
"version": "0.11.13",
"version": "0.11.14",
"description": "Timer scheduler module for Semantic Bus",
"private": true,
"main": "app.js",
Expand Down
Loading