From a10671630bd8dda694c503d3f88664f485677d8d Mon Sep 17 00:00:00 2001 From: Pujit Mehrotra Date: Wed, 27 Aug 2025 12:00:57 -0400 Subject: [PATCH 1/5] fix: correctly handle periods in share names --- api/src/__test__/store/modules/emhttp.test.ts | 211 +++++++++++++++++- api/src/core/utils/misc/parse-config.ts | 35 +++ .../initial-state/initial-config-state.ts | 0 3 files changed, 244 insertions(+), 2 deletions(-) delete mode 100644 api/src/store/initial-state/initial-config-state.ts diff --git a/api/src/__test__/store/modules/emhttp.test.ts b/api/src/__test__/store/modules/emhttp.test.ts index 793d9aa47c..c63e01a94a 100644 --- a/api/src/__test__/store/modules/emhttp.test.ts +++ b/api/src/__test__/store/modules/emhttp.test.ts @@ -1,7 +1,8 @@ -import { expect, test } from 'vitest'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { parseConfig } from '@app/core/utils/misc/parse-config.js'; import { store } from '@app/store/index.js'; -import { FileLoadStatus } from '@app/store/types.js'; +import { FileLoadStatus, StateFileKey } from '@app/store/types.js'; // Preloading imports for faster tests import '@app/store/modules/emhttp.js'; @@ -1110,3 +1111,209 @@ test('After init returns values from cfg file for all fields', { timeout: 30000 } `); }); + +describe('Share parsing with periods in names', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + test('parseConfig handles periods in INI section names', () => { + const mockIniContent = ` +["share.with.periods"] +name=share.with.periods +useCache=yes +include= +exclude= + +[normal_share] +name=normal_share +useCache=no +include= +exclude= +`; + + const result = parseConfig({ + file: mockIniContent, + type: 'ini', + }); + + // The result should now have properly flattened keys + + expect(result).toHaveProperty('shareWithPeriods'); + expect(result).toHaveProperty('normalShare'); + expect(result.shareWithPeriods.name).toBe('share.with.periods'); + expect(result.normalShare.name).toBe('normal_share'); + }); + + test('shares parser handles periods in share names correctly', async () => { + const { parse } = await import('@app/store/state-parsers/shares.js'); + + // The parser expects an object where values are share configs + const mockSharesState = { + shareWithPeriods: { + name: 'share.with.periods', + free: '1000000', + used: '500000', + size: '1500000', + include: '', + exclude: '', + useCache: 'yes', + }, + normalShare: { + name: 'normal_share', + free: '2000000', + used: '750000', + size: '2750000', + include: '', + exclude: '', + useCache: 'no', + }, + } as any; + + const result = parse(mockSharesState); + + expect(result).toHaveLength(2); + const periodShare = result.find((s) => s.name === 'share.with.periods'); + const normalShare = result.find((s) => s.name === 'normal_share'); + + expect(periodShare).toBeDefined(); + expect(periodShare?.id).toBe('share.with.periods'); + expect(periodShare?.name).toBe('share.with.periods'); + expect(periodShare?.cache).toBe(true); + + expect(normalShare).toBeDefined(); + expect(normalShare?.id).toBe('normal_share'); + expect(normalShare?.name).toBe('normal_share'); + expect(normalShare?.cache).toBe(false); + }); + + test('SMB parser handles periods in share names', async () => { + const { parse } = await import('@app/store/state-parsers/smb.js'); + + const mockSmbState = { + 'share.with.periods': { + export: 'e', + security: 'public', + writeList: '', + readList: '', + volsizelimit: '0', + }, + normal_share: { + export: 'e', + security: 'private', + writeList: 'user1,user2', + readList: '', + volsizelimit: '1000', + }, + } as any; + + const result = parse(mockSmbState); + + expect(result).toHaveLength(2); + const periodShare = result.find((s) => s.name === 'share.with.periods'); + const normalShare = result.find((s) => s.name === 'normal_share'); + + expect(periodShare).toBeDefined(); + expect(periodShare?.name).toBe('share.with.periods'); + expect(periodShare?.enabled).toBe(true); + + expect(normalShare).toBeDefined(); + expect(normalShare?.name).toBe('normal_share'); + expect(normalShare?.writeList).toEqual(['user1', 'user2']); + }); + + test('NFS parser handles periods in share names', async () => { + const { parse } = await import('@app/store/state-parsers/nfs.js'); + + const mockNfsState = { + 'share.with.periods': { + export: 'e', + security: 'public', + writeList: '', + readList: 'user1', + hostList: '', + }, + normal_share: { + export: 'd', + security: 'private', + writeList: 'user2', + readList: '', + hostList: '192.168.1.0/24', + }, + } as any; + + const result = parse(mockNfsState); + + expect(result).toHaveLength(2); + const periodShare = result.find((s) => s.name === 'share.with.periods'); + const normalShare = result.find((s) => s.name === 'normal_share'); + + expect(periodShare).toBeDefined(); + expect(periodShare?.name).toBe('share.with.periods'); + expect(periodShare?.enabled).toBe(true); + expect(periodShare?.readList).toEqual(['user1']); + + expect(normalShare).toBeDefined(); + expect(normalShare?.name).toBe('normal_share'); + expect(normalShare?.enabled).toBe(false); + }); +}); + +describe('Share lookup with periods in names', () => { + test('getShares finds user shares with periods in names', async () => { + // Mock the store state + const mockStore = await import('@app/store/index.js'); + const mockEmhttpState = { + shares: [ + { + id: 'share.with.periods', + name: 'share.with.periods', + cache: true, + free: 1000000, + used: 500000, + size: 1500000, + include: [], + exclude: [], + }, + { + id: 'normal_share', + name: 'normal_share', + cache: false, + free: 2000000, + used: 750000, + size: 2750000, + include: [], + exclude: [], + }, + ], + smbShares: [ + { name: 'share.with.periods', enabled: true, security: 'public' }, + { name: 'normal_share', enabled: true, security: 'private' }, + ], + nfsShares: [ + { name: 'share.with.periods', enabled: false }, + { name: 'normal_share', enabled: true }, + ], + disks: [], + }; + + const gettersSpy = vi.spyOn(mockStore, 'getters', 'get').mockReturnValue({ + emhttp: () => mockEmhttpState, + } as any); + + const { getShares } = await import('@app/core/utils/shares/get-shares.js'); + + const periodShare = getShares('user', { name: 'share.with.periods' }); + const normalShare = getShares('user', { name: 'normal_share' }); + + expect(periodShare).not.toBeNull(); + expect(periodShare?.name).toBe('share.with.periods'); + expect(periodShare?.type).toBe('user'); + + expect(normalShare).not.toBeNull(); + expect(normalShare?.name).toBe('normal_share'); + expect(normalShare?.type).toBe('user'); + + gettersSpy.mockRestore(); + }); +}); diff --git a/api/src/core/utils/misc/parse-config.ts b/api/src/core/utils/misc/parse-config.ts index 032fc94d99..93b944d131 100644 --- a/api/src/core/utils/misc/parse-config.ts +++ b/api/src/core/utils/misc/parse-config.ts @@ -23,6 +23,38 @@ type OptionsWithLoadedFile = { type: ConfigType; }; +/** + * Flattens nested objects that were incorrectly created by periods in INI section names. + * For example: { share: { with: { periods: {...} } } } -> { "share.with.periods": {...} } + */ +const flattenPeriodSections = (obj: Record, prefix = ''): Record => { + const result: Record = {}; + + for (const [key, value] of Object.entries(obj)) { + const fullKey = prefix ? `${prefix}.${key}` : key; + + if (value && typeof value === 'object' && !Array.isArray(value)) { + // Check if this looks like an INI section (has basic INI properties) + const hasIniProperties = Object.keys(value).some( + (k) => typeof value[k] === 'string' || typeof value[k] === 'number' + ); + + if (hasIniProperties) { + // This looks like an INI section, keep it as is + result[fullKey] = value; + } else { + // This looks like nested structure from periods, continue flattening + Object.assign(result, flattenPeriodSections(value, fullKey)); + } + } else { + // Regular property, keep as is + result[fullKey] = value; + } + } + + return result; +}; + /** * Converts the following * ``` @@ -127,6 +159,9 @@ export const parseConfig = >( let data: Record; try { data = parseIni(fileContents); + + // Fix nested objects created by periods in section names + data = flattenPeriodSections(data); } catch (error) { throw new AppError( `Failed to parse config file: ${error instanceof Error ? error.message : String(error)}` diff --git a/api/src/store/initial-state/initial-config-state.ts b/api/src/store/initial-state/initial-config-state.ts deleted file mode 100644 index e69de29bb2..0000000000 From 4955f46051242cf2288df868ef4f11f05e27fe20 Mon Sep 17 00:00:00 2001 From: Pujit Mehrotra Date: Wed, 27 Aug 2025 14:15:41 -0400 Subject: [PATCH 2/5] fix period flattener --- api/dev/states/shares.ini | 34 +++++++++++++ .../core/utils/shares/get-shares.test.ts | 42 +++++++++++++++ api/src/__test__/store/modules/emhttp.test.ts | 19 +++++++ .../store/state-parsers/shares.test.ts | 19 +++++++ api/src/core/utils/misc/parse-config.ts | 51 +++++++++++++++---- 5 files changed, 155 insertions(+), 10 deletions(-) diff --git a/api/dev/states/shares.ini b/api/dev/states/shares.ini index d4641f4536..988a0ac347 100644 --- a/api/dev/states/shares.ini +++ b/api/dev/states/shares.ini @@ -65,4 +65,38 @@ color="yellow-on" size="0" free="9091184" used="32831348" +luksStatus="0" +["system.with.periods"] +name="system.with.periods" +nameOrig="system.with.periods" +comment="system data with periods" +allocator="highwater" +splitLevel="1" +floor="0" +include="" +exclude="" +useCache="prefer" +cachePool="cache" +cow="auto" +color="yellow-on" +size="0" +free="9091184" +used="32831348" +luksStatus="0" +["system.with.🚀"] +name="system.with.🚀" +nameOrig="system.with.🚀" +comment="system data with 🚀" +allocator="highwater" +splitLevel="1" +floor="0" +include="" +exclude="" +useCache="prefer" +cachePool="cache" +cow="auto" +color="yellow-on" +size="0" +free="9091184" +used="32831348" luksStatus="0" \ No newline at end of file diff --git a/api/src/__test__/core/utils/shares/get-shares.test.ts b/api/src/__test__/core/utils/shares/get-shares.test.ts index 97667e9492..befbac8617 100644 --- a/api/src/__test__/core/utils/shares/get-shares.test.ts +++ b/api/src/__test__/core/utils/shares/get-shares.test.ts @@ -95,6 +95,27 @@ test('Returns both disk and user shares', async () => { "type": "user", "used": 33619300, }, + { + "allocator": "highwater", + "cachePool": "cache", + "color": "yellow-on", + "comment": "system data with periods", + "cow": "auto", + "exclude": [], + "floor": "0", + "free": 9309372, + "id": "system.with.periods", + "include": [], + "luksStatus": "0", + "name": "system.with.periods", + "nameOrig": "system.with.periods", + "nfs": {}, + "size": 0, + "smb": {}, + "splitLevel": "1", + "type": "user", + "used": 33619300, + }, ], } `); @@ -211,6 +232,27 @@ test('Returns shares by type', async () => { "type": "user", "used": 33619300, }, + { + "allocator": "highwater", + "cachePool": "cache", + "color": "yellow-on", + "comment": "system data with periods", + "cow": "auto", + "exclude": [], + "floor": "0", + "free": 9309372, + "id": "system.with.periods", + "include": [], + "luksStatus": "0", + "name": "system.with.periods", + "nameOrig": "system.with.periods", + "nfs": {}, + "size": 0, + "smb": {}, + "splitLevel": "1", + "type": "user", + "used": 33619300, + }, ] `); expect(getShares('disk')).toMatchInlineSnapshot('null'); diff --git a/api/src/__test__/store/modules/emhttp.test.ts b/api/src/__test__/store/modules/emhttp.test.ts index c63e01a94a..52d07f756a 100644 --- a/api/src/__test__/store/modules/emhttp.test.ts +++ b/api/src/__test__/store/modules/emhttp.test.ts @@ -447,6 +447,25 @@ test('After init returns values from cfg file for all fields', { timeout: 30000 "splitLevel": "1", "used": 33619300, }, + { + "allocator": "highwater", + "cache": false, + "cachePool": "cache", + "color": "yellow-on", + "comment": "system data with periods", + "cow": "auto", + "exclude": [], + "floor": "0", + "free": 9309372, + "id": "system.with.periods", + "include": [], + "luksStatus": "0", + "name": "system.with.periods", + "nameOrig": "system.with.periods", + "size": 0, + "splitLevel": "1", + "used": 33619300, + }, ] `); expect(nfsShares).toMatchInlineSnapshot(` diff --git a/api/src/__test__/store/state-parsers/shares.test.ts b/api/src/__test__/store/state-parsers/shares.test.ts index 6435a72edb..fb43380348 100644 --- a/api/src/__test__/store/state-parsers/shares.test.ts +++ b/api/src/__test__/store/state-parsers/shares.test.ts @@ -92,6 +92,25 @@ test('Returns parsed state file', async () => { "splitLevel": "1", "used": 33619300, }, + { + "allocator": "highwater", + "cache": false, + "cachePool": "cache", + "color": "yellow-on", + "comment": "system data with periods", + "cow": "auto", + "exclude": [], + "floor": "0", + "free": 9309372, + "id": "system.with.periods", + "include": [], + "luksStatus": "0", + "name": "system.with.periods", + "nameOrig": "system.with.periods", + "size": 0, + "splitLevel": "1", + "used": 33619300, + }, ] `); }); diff --git a/api/src/core/utils/misc/parse-config.ts b/api/src/core/utils/misc/parse-config.ts index 93b944d131..6a12bfd47e 100644 --- a/api/src/core/utils/misc/parse-config.ts +++ b/api/src/core/utils/misc/parse-config.ts @@ -25,7 +25,11 @@ type OptionsWithLoadedFile = { /** * Flattens nested objects that were incorrectly created by periods in INI section names. - * For example: { share: { with: { periods: {...} } } } -> { "share.with.periods": {...} } + * For example: { system: { with: { periods: {...} } } } -> { "system.with.periods": {...} } + * + * The strategy is: + * 1. If a nested object has string/number properties directly, it's likely an INI section + * 2. If it only has nested objects, it's structure created by periods and should be flattened */ const flattenPeriodSections = (obj: Record, prefix = ''): Record => { const result: Record = {}; @@ -34,17 +38,45 @@ const flattenPeriodSections = (obj: Record, prefix = ''): Record typeof value[k] === 'string' || typeof value[k] === 'number' + // Check if this object has any direct string/number properties (INI section properties) + const hasDirectProperties = Object.entries(value).some( + ([k, v]) => typeof v === 'string' || typeof v === 'number' ); - if (hasIniProperties) { - // This looks like an INI section, keep it as is - result[fullKey] = value; - } else { - // This looks like nested structure from periods, continue flattening + // Check if this object has nested objects (potential period structure) + const hasNestedObjects = Object.entries(value).some( + ([k, v]) => v && typeof v === 'object' && !Array.isArray(v) + ); + + if (hasDirectProperties) { + // This has direct properties, treat as an INI section + // But we still need to check for nested structures within it + const sectionProperties: Record = {}; + const nestedStructures: Record = {}; + + for (const [propKey, propValue] of Object.entries(value)) { + if (propValue && typeof propValue === 'object' && !Array.isArray(propValue)) { + nestedStructures[propKey] = propValue; + } else { + sectionProperties[propKey] = propValue; + } + } + + // Keep the direct properties as a section + if (Object.keys(sectionProperties).length > 0) { + result[fullKey] = sectionProperties; + } + + // Flatten any nested structures + if (Object.keys(nestedStructures).length > 0) { + Object.assign(result, flattenPeriodSections(nestedStructures, fullKey)); + } + } else if (hasNestedObjects) { + // This only has nested objects, continue flattening Object.assign(result, flattenPeriodSections(value, fullKey)); + } else { + // Empty object or other case + result[fullKey] = value; } } else { // Regular property, keep as is @@ -159,7 +191,6 @@ export const parseConfig = >( let data: Record; try { data = parseIni(fileContents); - // Fix nested objects created by periods in section names data = flattenPeriodSections(data); } catch (error) { From e5a1a50c7442188833d944c80d43159c5c99cdde Mon Sep 17 00:00:00 2001 From: Pujit Mehrotra Date: Wed, 27 Aug 2025 14:21:53 -0400 Subject: [PATCH 3/5] add emoji test --- .../core/utils/shares/get-shares.test.ts | 42 +++++++++++++++++++ api/src/__test__/store/modules/emhttp.test.ts | 19 +++++++++ .../store/state-parsers/shares.test.ts | 19 +++++++++ 3 files changed, 80 insertions(+) diff --git a/api/src/__test__/core/utils/shares/get-shares.test.ts b/api/src/__test__/core/utils/shares/get-shares.test.ts index befbac8617..df6d04010d 100644 --- a/api/src/__test__/core/utils/shares/get-shares.test.ts +++ b/api/src/__test__/core/utils/shares/get-shares.test.ts @@ -116,6 +116,27 @@ test('Returns both disk and user shares', async () => { "type": "user", "used": 33619300, }, + { + "allocator": "highwater", + "cachePool": "cache", + "color": "yellow-on", + "comment": "system data with 🚀", + "cow": "auto", + "exclude": [], + "floor": "0", + "free": 9309372, + "id": "system.with.🚀", + "include": [], + "luksStatus": "0", + "name": "system.with.🚀", + "nameOrig": "system.with.🚀", + "nfs": {}, + "size": 0, + "smb": {}, + "splitLevel": "1", + "type": "user", + "used": 33619300, + }, ], } `); @@ -253,6 +274,27 @@ test('Returns shares by type', async () => { "type": "user", "used": 33619300, }, + { + "allocator": "highwater", + "cachePool": "cache", + "color": "yellow-on", + "comment": "system data with 🚀", + "cow": "auto", + "exclude": [], + "floor": "0", + "free": 9309372, + "id": "system.with.🚀", + "include": [], + "luksStatus": "0", + "name": "system.with.🚀", + "nameOrig": "system.with.🚀", + "nfs": {}, + "size": 0, + "smb": {}, + "splitLevel": "1", + "type": "user", + "used": 33619300, + }, ] `); expect(getShares('disk')).toMatchInlineSnapshot('null'); diff --git a/api/src/__test__/store/modules/emhttp.test.ts b/api/src/__test__/store/modules/emhttp.test.ts index 52d07f756a..cc63e7341d 100644 --- a/api/src/__test__/store/modules/emhttp.test.ts +++ b/api/src/__test__/store/modules/emhttp.test.ts @@ -466,6 +466,25 @@ test('After init returns values from cfg file for all fields', { timeout: 30000 "splitLevel": "1", "used": 33619300, }, + { + "allocator": "highwater", + "cache": false, + "cachePool": "cache", + "color": "yellow-on", + "comment": "system data with 🚀", + "cow": "auto", + "exclude": [], + "floor": "0", + "free": 9309372, + "id": "system.with.🚀", + "include": [], + "luksStatus": "0", + "name": "system.with.🚀", + "nameOrig": "system.with.🚀", + "size": 0, + "splitLevel": "1", + "used": 33619300, + }, ] `); expect(nfsShares).toMatchInlineSnapshot(` diff --git a/api/src/__test__/store/state-parsers/shares.test.ts b/api/src/__test__/store/state-parsers/shares.test.ts index fb43380348..08405a3478 100644 --- a/api/src/__test__/store/state-parsers/shares.test.ts +++ b/api/src/__test__/store/state-parsers/shares.test.ts @@ -111,6 +111,25 @@ test('Returns parsed state file', async () => { "splitLevel": "1", "used": 33619300, }, + { + "allocator": "highwater", + "cache": false, + "cachePool": "cache", + "color": "yellow-on", + "comment": "system data with 🚀", + "cow": "auto", + "exclude": [], + "floor": "0", + "free": 9309372, + "id": "system.with.🚀", + "include": [], + "luksStatus": "0", + "name": "system.with.🚀", + "nameOrig": "system.with.🚀", + "size": 0, + "splitLevel": "1", + "used": 33619300, + }, ] `); }); From 47bec7febcbe44d17ac5b4b79b9b5c15a05ec285 Mon Sep 17 00:00:00 2001 From: Pujit Mehrotra Date: Wed, 27 Aug 2025 14:29:45 -0400 Subject: [PATCH 4/5] simplify flattener --- api/src/core/utils/misc/parse-config.ts | 72 +++++++++---------------- 1 file changed, 26 insertions(+), 46 deletions(-) diff --git a/api/src/core/utils/misc/parse-config.ts b/api/src/core/utils/misc/parse-config.ts index 6a12bfd47e..f8aa121b52 100644 --- a/api/src/core/utils/misc/parse-config.ts +++ b/api/src/core/utils/misc/parse-config.ts @@ -26,61 +26,41 @@ type OptionsWithLoadedFile = { /** * Flattens nested objects that were incorrectly created by periods in INI section names. * For example: { system: { with: { periods: {...} } } } -> { "system.with.periods": {...} } - * - * The strategy is: - * 1. If a nested object has string/number properties directly, it's likely an INI section - * 2. If it only has nested objects, it's structure created by periods and should be flattened */ const flattenPeriodSections = (obj: Record, prefix = ''): Record => { const result: Record = {}; + const isNestedObject = (value: unknown) => + Boolean(value && typeof value === 'object' && !Array.isArray(value)); for (const [key, value] of Object.entries(obj)) { const fullKey = prefix ? `${prefix}.${key}` : key; - if (value && typeof value === 'object' && !Array.isArray(value)) { - // Check if this object has any direct string/number properties (INI section properties) - const hasDirectProperties = Object.entries(value).some( - ([k, v]) => typeof v === 'string' || typeof v === 'number' - ); - - // Check if this object has nested objects (potential period structure) - const hasNestedObjects = Object.entries(value).some( - ([k, v]) => v && typeof v === 'object' && !Array.isArray(v) - ); - - if (hasDirectProperties) { - // This has direct properties, treat as an INI section - // But we still need to check for nested structures within it - const sectionProperties: Record = {}; - const nestedStructures: Record = {}; - - for (const [propKey, propValue] of Object.entries(value)) { - if (propValue && typeof propValue === 'object' && !Array.isArray(propValue)) { - nestedStructures[propKey] = propValue; - } else { - sectionProperties[propKey] = propValue; - } - } - - // Keep the direct properties as a section - if (Object.keys(sectionProperties).length > 0) { - result[fullKey] = sectionProperties; - } - - // Flatten any nested structures - if (Object.keys(nestedStructures).length > 0) { - Object.assign(result, flattenPeriodSections(nestedStructures, fullKey)); - } - } else if (hasNestedObjects) { - // This only has nested objects, continue flattening - Object.assign(result, flattenPeriodSections(value, fullKey)); + if (!isNestedObject(value)) { + result[fullKey] = value; + continue; + } + + const section = {}; + const nestedObjs = {}; + let hasSectionProps = false; + + for (const [propKey, propValue] of Object.entries(value)) { + if (isNestedObject(propValue)) { + nestedObjs[propKey] = propValue; } else { - // Empty object or other case - result[fullKey] = value; + section[propKey] = propValue; + hasSectionProps = true; } - } else { - // Regular property, keep as is - result[fullKey] = value; + } + + // Process direct properties first to maintain order + if (hasSectionProps) { + result[fullKey] = section; + } + + // Then process nested objects + if (Object.keys(nestedObjs).length > 0) { + Object.assign(result, flattenPeriodSections(nestedObjs, fullKey)); } } From 2ae9083703a2241aed0a0a8bcf98c74f98a5d725 Mon Sep 17 00:00:00 2001 From: Pujit Mehrotra Date: Wed, 27 Aug 2025 15:01:00 -0400 Subject: [PATCH 5/5] harden against prototype pollution --- api/src/__test__/store/modules/emhttp.test.ts | 2 +- api/src/core/utils/misc/parse-config.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/api/src/__test__/store/modules/emhttp.test.ts b/api/src/__test__/store/modules/emhttp.test.ts index cc63e7341d..743397913d 100644 --- a/api/src/__test__/store/modules/emhttp.test.ts +++ b/api/src/__test__/store/modules/emhttp.test.ts @@ -2,7 +2,7 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; import { parseConfig } from '@app/core/utils/misc/parse-config.js'; import { store } from '@app/store/index.js'; -import { FileLoadStatus, StateFileKey } from '@app/store/types.js'; +import { FileLoadStatus } from '@app/store/types.js'; // Preloading imports for faster tests import '@app/store/modules/emhttp.js'; diff --git a/api/src/core/utils/misc/parse-config.ts b/api/src/core/utils/misc/parse-config.ts index f8aa121b52..5aadc19b0e 100644 --- a/api/src/core/utils/misc/parse-config.ts +++ b/api/src/core/utils/misc/parse-config.ts @@ -31,8 +31,11 @@ const flattenPeriodSections = (obj: Record, prefix = ''): Record = {}; const isNestedObject = (value: unknown) => Boolean(value && typeof value === 'object' && !Array.isArray(value)); + // prevent prototype pollution/injection + const isUnsafeKey = (k: string) => k === '__proto__' || k === 'prototype' || k === 'constructor'; for (const [key, value] of Object.entries(obj)) { + if (isUnsafeKey(key)) continue; const fullKey = prefix ? `${prefix}.${key}` : key; if (!isNestedObject(value)) { @@ -45,6 +48,7 @@ const flattenPeriodSections = (obj: Record, prefix = ''): Record