Skip to content

922 feature request restrict access to get item and get stock to the clans to which they belong - #962

Merged
CapoMK25 merged 10 commits into
devfrom
922-feature-request-restrict-access-to-get-item-and-get-stock-to-the-clans-to-which-they-belong
Sep 8, 2026
Merged

922 feature request restrict access to get item and get stock to the clans to which they belong#962
CapoMK25 merged 10 commits into
devfrom
922-feature-request-restrict-access-to-get-item-and-get-stock-to-the-clans-to-which-they-belong

Conversation

@constf03

@constf03 constf03 commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Brief description

Restricts read access to endpoints GET /item/:id and GET /stock/:id to logged-in Players who belong in the same Clan as the Item and Stock objects (same clan_id).

Change list

  • stock.service.ts: added new helper method for retrieving Stock's clan_id (similar with already existing ItemHelperService.getItemClanId())
  • item.controller.ts & stock.controller.ts: included a 403 in the errors array and @ LoggedUser() parameter decorator
  • item.controller.ts & stock.controller.ts: added a condition that checks that the logged-in Player's and Item's/Stock's clan_id mathes, otherwise return 403 APIError.

Tests

Tested the GET /item/:id and GET /stock/:id routes via in-memory testing. The script initializes MongoMemoryServer and required Item, Stock, Player modules and runs the Item controller and Stock controller get() methods with result logs. Below is the script and result.

Script:

require('dotenv').config();

(async () => {
  const { MongoMemoryServer } = require('mongodb-memory-server');

  const mem = await MongoMemoryServer.create();
  const mongoose = require('mongoose');
  await mongoose.connect(mem.getUri(), { dbName: 'probe' });

  const { ObjectId } = require('mongodb');

  const Player = mongoose.model(
    'Player',
    require('./src/player/schemas/player.schema').PlayerSchema,
  );
  const Stock = mongoose.model(
    'Stock',
    require('./src/clanInventory/stock/stock.schema').StockSchema,
  );
  const Item = mongoose.model(
    'Item',
    require('./src/clanInventory/item/item.schema').ItemSchema,
  );
  const Clan = mongoose.model(
    'Clan',
    require('./src/clan/clan.schema').ClanSchema,
  );

  const itemService =
    new (require('./src/clanInventory/item/item.service').ItemService)(Item);
  const stockService =
    new (require('./src/clanInventory/stock/stock.service').StockService)(
      Stock,
      Player,
      itemService,
    );
  const helperService =
    new (require('./src/clanInventory/item/itemHelper.service').ItemHelperService)(
      Clan,
      itemService,
      null,
    );

  const stockController =
    new (require('./src/clanInventory/stock/stock.controller').StockController)(
      stockService,
      Player,
    );

  const itemController =
    new (require('./src/clanInventory/item/item.controller').ItemController)(
      itemService,
      helperService,
      null,
      null,
      Player,
    );

  const User = require('./src/auth/user').User;

  const idA = new ObjectId();
  const idB = new ObjectId();

  const player = await Player.create({
    name: 'probe',
    uniqueIdentifier: 'u1',
    clan_id: idA,
    profile_id: new ObjectId(),
    backpackCapacity: 10,
    points: 0,
    battlePoints: 0,
    above13: true,
    parentalAuth: true,
    currentAvatarId: 101,
  });

  const user = new User(
    player.profile_id.toString(),
    player._id.toString(),
    idA.toString(),
  );

  const stockA = await Stock.create({ cellCount: 10, clan_id: idA });
  const stockB = await Stock.create({ cellCount: 10, clan_id: idB });

  const base = {
    name: 'Armchair_Rakkaus',
    weight: 1,
    recycling: 'glass',
    rarity: 'common',
    unityKey: 'k',
    location: [-1, -1],
    furnitureSize: [1, 1],
    price: 10,
    isFurniture: true,
  };

  const itemA = await Item.create(
    Object.assign({}, base, { stock_id: stockA._id }),
  );
  const itemB = await Item.create(
    Object.assign({}, base, { stock_id: stockB._id }),
  );

  const show = async (title, payload) => {
    const result = await payload;
    const errors = result && result[1];
    if (errors)
      console.log(
        title,
        '->',
        errors[0].reason,
        'status=' + (errors[0].statusCode || 'n/a'),
      );
    else
      console.log(title, '-> 200 OK', JSON.stringify(result[0]).slice(0, 55));
  };

  console.log('--- StockController.get ---');
  await show(
    'own clan: ',
    stockController.get({ _id: stockA._id.toString() }, user),
  );
  await show(
    'other clan: ',
    stockController.get({ _id: stockB._id.toString() }, user),
  );
  await show(
    'missing: ',
    stockController.get({ _id: '000000000000000000000000' }, user),
  );

  console.log('--- ItemController.get ---');
  await show(
    'own clan: ',
    itemController.get({ _id: itemA._id.toString() }, user),
  );
  await show(
    'other clan: ',
    itemController.get({ _id: itemB._id.toString() }, user),
  );
  await show(
    'missing: ',
    itemController.get({ _id: '000000000000000000000000' }, user),
  );

  await mongoose.disconnect();
  await mem.stop();
})().catch((error) => {
  console.error('FAILED:', error);
});

Result:

PS C:\ProgrammingStuff\Altzone-Server> npx ts-node ./test.ts
[dotenv@17.2.3] injecting env (28) from .env -- tip: 👥 sync secrets across teammates & machines: https://dotenvx.com/ops
[dotenv@17.2.3] injecting env (0) from .env -- tip: 🔐 prevent committing .env to code: https://dotenvx.com/precommit
--- StockController.get ---
own clan:  -> 200 OK [{"_id":"6a9fd824250c6bb592b15d4f","name":"Armchair_Rak
other clan:  -> NOT_AUTHORIZED status=403
missing:  -> NOT_FOUND status=n/a
--- ItemController.get ---
own clan:  -> 200 OK {"_id":"6a9fd824250c6bb592b15d4f","name":"Armchair_Rakk
other clan:  -> NOT_AUTHORIZED status=403
missing:  -> NOT_FOUND status=n/a
PS C:\ProgrammingStuff\Altzone-Server>

I didn't write it inline because there was module/import errors however running the script from a temporary .ts file worked (hence the ./test.ts). Also for the "missing" status it shows n/a instead of 404 because the Item controller and Stock controller do not directly build a 404 error instead it comes from BasicService.readOneById.

@codecov-alt

codecov-alt Bot commented Sep 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
src/clanInventory/stock/stock.service.ts 92.39% <100.00%> (+0.34%) ⬆️

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@constf03

constf03 commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

I'll add unit test for the new helper method I forgot

@CapoMK25 CapoMK25 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Solid work, approved!

@github-project-automation github-project-automation Bot moved this from Backlog to Done in Altzone-Server Sep 5, 2026
@CapoMK25

CapoMK25 commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

One question before merging: has this been tested via the backend on Postman/Bruno/the terminal (in-memory testing/other methods)? @constf03

@constf03

constf03 commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

One question before merging: has this been tested via the backend on Postman/Bruno/the terminal (in-memory testing/other methods)? @constf03

Sorry I should have asked about this before but how to properly test authenticated routes in postman? Is there a seeder script that populates the local docker db tables with test data or do I have to manually do the flow of creating 2 players, then login via /auth, create 2 clans and their respective Stock and Item objects?

@CapoMK25

CapoMK25 commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

I've always done it manually, and hated it. The auth is valid for a longer-than-average time period, but setting up is a PITA.

That's why in smaller issues I've resorted to this:

[capomk@lianli Altzone-Server]$ npx ts-node -e "
import { hasUnsafeMongoUpdateKey, validateMongoUpdate } from './src/common/function/validateMongoUpdate';

const cleanPayload = { \$set: { name: 'PlayerOne', level: 10 } };
const prototypeAttack = JSON.parse('{\"__proto__\": {\"admin\": true}}');
const dotNotationAttack = { \$set: { '__proto__.admin': true } };
const nestedAttack = { \$set: { profile: { 'constructor.prototype.role': 'admin' } } };

console.log('--- CVE-2026-73562 Validation Tests ---');
console.log('1. Clean Payload (Expected: false) ->', hasUnsafeMongoUpdateKey(cleanPayload));
console.log('2. JSON Prototype Attack (Expected: true) ->', hasUnsafeMongoUpdateKey(prototypeAttack));
console.log('3. Dot Notation Attack (Expected: true) ->', hasUnsafeMongoUpdateKey(dotNotationAttack));
console.log('4. Deeply Nested Attack (Expected: true) ->', hasUnsafeMongoUpdateKey(nestedAttack));

try {
  validateMongoUpdate(dotNotationAttack);
} catch (err: any) {
  console.log('5. Exception Handler Test -> Caught expected error:', err.message);
}
"
npm notice run altzone-server@1.0.0 npx
npm notice run 'ts-node' -e 
npm notice run import { hasUnsafeMongoUpdateKey, validateMongoUpdate } from './src/common/function/validateMongoUpdate';
npm notice run
npm notice run const cleanPayload = { $set: { name: 'PlayerOne', level: 10 } };
npm notice run const prototypeAttack = JSON.parse('{"__proto__": {"admin": true}}');
npm notice run const dotNotationAttack = { $set: { '__proto__.admin': true } };
npm notice run const nestedAttack = { $set: { profile: { 'constructor.prototype.role': 'admin' } } };
npm notice run
npm notice run console.log('--- CVE-2026-73562 Validation Tests ---');
npm notice run console.log('1. Clean Payload (Expected: false) ->', hasUnsafeMongoUpdateKey(cleanPayload));
npm notice run console.log('2. JSON Prototype Attack (Expected: true) ->', hasUnsafeMongoUpdateKey(prototypeAttack));
npm notice run console.log('3. Dot Notation Attack (Expected: true) ->', hasUnsafeMongoUpdateKey(dotNotationAttack));
npm notice run console.log('4. Deeply Nested Attack (Expected: true) ->', hasUnsafeMongoUpdateKey(nestedAttack));
npm notice run
npm notice run try {
npm notice run   validateMongoUpdate(dotNotationAttack);
npm notice run } catch (err: any) {
npm notice run   console.log('5. Exception Handler Test -> Caught expected error:', err.message);
npm notice run }
--- CVE-2026-73562 Validation Tests ---
1. Clean Payload (Expected: false) -> false
2. JSON Prototype Attack (Expected: true) -> true
3. Dot Notation Attack (Expected: true) -> true
4. Deeply Nested Attack (Expected: true) -> true
5. Exception Handler Test -> Caught expected error: Invalid or dangerous update key detected
[capomk@lianli Altzone-Server]$

This method is called isolated script testing (or in-memory testing). It should be applicable here as well. On the seeder script, that's a good "nosto", I'll remember to bring it up or create an issue for it, since currently the setup is indeed time-consuming and tedious if you want to go the full Postman/Bruno route.

@constf03

constf03 commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

I've always done it manually, and hated it. The auth is valid for a longer-than-average time period, but setting up is a PITA.

That's why in smaller issues I've resorted to this:

[capomk@lianli Altzone-Server]$ npx ts-node -e "
import { hasUnsafeMongoUpdateKey, validateMongoUpdate } from './src/common/function/validateMongoUpdate';

const cleanPayload = { \$set: { name: 'PlayerOne', level: 10 } };
const prototypeAttack = JSON.parse('{\"__proto__\": {\"admin\": true}}');
const dotNotationAttack = { \$set: { '__proto__.admin': true } };
const nestedAttack = { \$set: { profile: { 'constructor.prototype.role': 'admin' } } };

console.log('--- CVE-2026-73562 Validation Tests ---');
console.log('1. Clean Payload (Expected: false) ->', hasUnsafeMongoUpdateKey(cleanPayload));
console.log('2. JSON Prototype Attack (Expected: true) ->', hasUnsafeMongoUpdateKey(prototypeAttack));
console.log('3. Dot Notation Attack (Expected: true) ->', hasUnsafeMongoUpdateKey(dotNotationAttack));
console.log('4. Deeply Nested Attack (Expected: true) ->', hasUnsafeMongoUpdateKey(nestedAttack));

try {
  validateMongoUpdate(dotNotationAttack);
} catch (err: any) {
  console.log('5. Exception Handler Test -> Caught expected error:', err.message);
}
"
npm notice run altzone-server@1.0.0 npx
npm notice run 'ts-node' -e 
npm notice run import { hasUnsafeMongoUpdateKey, validateMongoUpdate } from './src/common/function/validateMongoUpdate';
npm notice run
npm notice run const cleanPayload = { $set: { name: 'PlayerOne', level: 10 } };
npm notice run const prototypeAttack = JSON.parse('{"__proto__": {"admin": true}}');
npm notice run const dotNotationAttack = { $set: { '__proto__.admin': true } };
npm notice run const nestedAttack = { $set: { profile: { 'constructor.prototype.role': 'admin' } } };
npm notice run
npm notice run console.log('--- CVE-2026-73562 Validation Tests ---');
npm notice run console.log('1. Clean Payload (Expected: false) ->', hasUnsafeMongoUpdateKey(cleanPayload));
npm notice run console.log('2. JSON Prototype Attack (Expected: true) ->', hasUnsafeMongoUpdateKey(prototypeAttack));
npm notice run console.log('3. Dot Notation Attack (Expected: true) ->', hasUnsafeMongoUpdateKey(dotNotationAttack));
npm notice run console.log('4. Deeply Nested Attack (Expected: true) ->', hasUnsafeMongoUpdateKey(nestedAttack));
npm notice run
npm notice run try {
npm notice run   validateMongoUpdate(dotNotationAttack);
npm notice run } catch (err: any) {
npm notice run   console.log('5. Exception Handler Test -> Caught expected error:', err.message);
npm notice run }
--- CVE-2026-73562 Validation Tests ---
1. Clean Payload (Expected: false) -> false
2. JSON Prototype Attack (Expected: true) -> true
3. Dot Notation Attack (Expected: true) -> true
4. Deeply Nested Attack (Expected: true) -> true
5. Exception Handler Test -> Caught expected error: Invalid or dangerous update key detected
[capomk@lianli Altzone-Server]$

This method is called isolated script testing (or in-memory testing). It should be applicable here as well. On the seeder script, that's a good "nosto", I'll remember to bring it up or create an issue for it, since currently the setup is indeed time-consuming and tedious if you want to go the full Postman/Bruno route.

👍Good to know thanks, I'll try something like this or manually do it on Postman, until then this PR can wait

@constf03

constf03 commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

@CapoMK25 Tested it in-memory and seems to work as intended, results in the PR description

@CapoMK25
CapoMK25 merged commit 208083c into dev Sep 8, 2026
5 checks passed
@CapoMK25
CapoMK25 deleted the 922-feature-request-restrict-access-to-get-item-and-get-stock-to-the-clans-to-which-they-belong branch September 8, 2026 14:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Feature request: Restrict access to GET /item and GET /stock to the clans to which they belong

2 participants