Explorar el Código

chore: Satisfy CodeQL security warnings (#3659)

Co-authored-by: Housein Abo Shaar <76689341+GogoIsProgramming@users.noreply.github.com>
Housein Abo Shaar hace 6 meses
padre
commit
b34751df1d
Se han modificado 36 ficheros con 263 adiciones y 155 borrados
  1. 6 0
      .github/workflows/build_and_test.yml
  2. 2 0
      .github/workflows/codegen.yml
  3. 2 0
      .github/workflows/deploy_dashboard.yml
  4. 22 20
      .github/workflows/docsearch.yml
  5. 2 0
      .github/workflows/publish_and_install.yml
  6. 1 2
      packages/admin-ui/src/lib/core/src/shared/components/asset-preview/asset-preview.component.ts
  7. 16 6
      packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/prosemirror/inputrules.ts
  8. 34 13
      packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/prosemirror/keymap.ts
  9. 35 13
      packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/prosemirror/menu/menu.ts
  10. 1 2
      packages/admin-ui/src/lib/customer/src/components/customer-group-detail/customer-group-detail.component.ts
  11. 0 1
      packages/admin-ui/src/lib/order/src/components/order-history/order-history.component.ts
  12. 0 2
      packages/admin-ui/src/lib/react/src/react-hooks/use-route-params.ts
  13. 1 3
      packages/admin-ui/src/lib/settings/src/components/shipping-method-detail/shipping-method-detail.component.ts
  14. 4 5
      packages/admin-ui/src/lib/settings/src/components/tax-rate-detail/tax-rate-detail.component.ts
  15. 12 4
      packages/asset-server-plugin/src/asset-server.ts
  16. 25 0
      packages/core/src/common/safe-assign.ts
  17. 10 3
      packages/core/src/config/merge-config.ts
  18. 11 5
      packages/core/src/entity/validate-custom-fields-config.ts
  19. 20 4
      packages/core/src/service/helpers/entity-hydrator/merge-deep.ts
  20. 0 1
      packages/dashboard/src/app/routes/_authenticated/_orders/components/customer-address-selector.tsx
  21. 6 2
      packages/dashboard/src/app/routes/_authenticated/_orders/components/payment-details.tsx
  22. 2 2
      packages/dashboard/src/app/routes/_authenticated/_orders/components/state-transition-control.tsx
  23. 0 1
      packages/dashboard/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx
  24. 1 1
      packages/dashboard/src/app/routes/_authenticated/_products/components/option-value-input.tsx
  25. 0 7
      packages/dashboard/src/app/routes/_authenticated/_zones/components/zone-countries-table.tsx
  26. 1 1
      packages/dashboard/src/lib/components/layout/content-language-selector.tsx
  27. 0 6
      packages/dashboard/src/lib/components/shared/asset/asset-preview.tsx
  28. 1 1
      packages/dashboard/src/lib/components/shared/option-value-input.tsx
  29. 1 1
      packages/dashboard/src/lib/components/shared/product-variant-selector.tsx
  30. 1 1
      packages/dashboard/src/lib/components/ui/calendar.tsx
  31. 1 1
      packages/dashboard/src/lib/framework/dashboard-widget/metrics-widget/index.tsx
  32. 0 2
      packages/dashboard/src/lib/framework/dashboard-widget/orders-summary/index.tsx
  33. 32 30
      packages/dashboard/src/lib/hooks/use-extended-list-query.ts
  34. 2 2
      packages/dashboard/vite/utils/config-loader.ts
  35. 0 1
      packages/email-plugin/src/plugin.ts
  36. 11 12
      scripts/codegen/plugins/graphql-errors-plugin.ts

+ 6 - 0
.github/workflows/build_and_test.yml

@@ -33,6 +33,8 @@ jobs:
     build:
         name: build
         runs-on: ubuntu-latest
+        permissions:
+            contents: read
         strategy:
             matrix:
                 node: [20.x, 22.x, 24.x]
@@ -51,6 +53,8 @@ jobs:
     unit-tests:
         name: unit tests
         runs-on: ubuntu-latest
+        permissions:
+            contents: read
         strategy:
             matrix:
                 node: [20.x, 22.x, 24.x]
@@ -71,6 +75,8 @@ jobs:
     e2e-tests:
         name: e2e tests
         runs-on: ubuntu-latest
+        permissions:
+            contents: read
         services:
             mariadb:
                 # With v11.6.2+, a default was changed, (https://mariadb.com/kb/en/innodb-system-variables/#innodb_snapshot_isolation)

+ 2 - 0
.github/workflows/codegen.yml

@@ -9,6 +9,8 @@ env:
 jobs:
     codegen:
         runs-on: ubuntu-latest
+        permissions:
+            contents: read
         steps:
             - uses: actions/checkout@v4
             - name: Use Node.js 22

+ 2 - 0
.github/workflows/deploy_dashboard.yml

@@ -26,6 +26,8 @@ concurrency:
 jobs:
     deploy:
         runs-on: ubuntu-latest
+        permissions:
+            contents: read
         env:
             # VERCEL_ENV: ${{ github.ref_name == 'minor' && 'development' || github.ref_name == 'major' && 'major' || 'production' }}
             VERCEL_ENV: production

+ 22 - 20
.github/workflows/docsearch.yml

@@ -1,26 +1,28 @@
 name: Index docs to Typesense
 
 on:
-  push:
-    branches:
-      - master
-    paths:
-      - 'docs/**'
-    
+    push:
+        branches:
+            - master
+        paths:
+            - 'docs/**'
+
 jobs:
-  index_docs:
-    runs-on: ubuntu-latest
+    index_docs:
+        runs-on: ubuntu-latest
+        permissions:
+            contents: read
 
-    steps:
-      - name: Checkout code
-        uses: actions/checkout@v2
+        steps:
+            - name: Checkout code
+              uses: actions/checkout@v4
 
-      - name: Index docs to Typesense
-        run: |
-          docker run \
-            -e TYPESENSE_API_KEY=${{ vars.TYPESENSE_API_KEY }} \
-            -e TYPESENSE_HOST="${{ vars.TYPESENSE_HOST }}" \
-            -e TYPESENSE_PORT="443" \
-            -e TYPESENSE_PROTOCOL="https" \
-            -e CONFIG="$(cat docs/scraper/config.json | jq -r tostring)" \
-            typesense/docsearch-scraper
+            - name: Index docs to Typesense
+              run: |
+                  docker run \
+                    -e TYPESENSE_API_KEY=${{ vars.TYPESENSE_API_KEY }} \
+                    -e TYPESENSE_HOST="${{ vars.TYPESENSE_HOST }}" \
+                    -e TYPESENSE_PORT="443" \
+                    -e TYPESENSE_PROTOCOL="https" \
+                    -e CONFIG="$(cat docs/scraper/config.json | jq -r tostring)" \
+                    typesense/docsearch-scraper

+ 2 - 0
.github/workflows/publish_and_install.yml

@@ -31,6 +31,8 @@ concurrency:
 jobs:
     publish_install:
         runs-on: ${{ matrix.os }}
+        permissions:
+            contents: read
         strategy:
             matrix:
                 os: [ubuntu-latest, windows-latest, macos-latest]

+ 1 - 2
packages/admin-ui/src/lib/core/src/shared/components/asset-preview/asset-preview.component.ts

@@ -10,7 +10,7 @@ import {
     Output,
     ViewChild,
 } from '@angular/core';
-import { FormBuilder, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
+import { FormBuilder, UntypedFormGroup } from '@angular/forms';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
 import { fromEvent, Subscription } from 'rxjs';
 import { debounceTime } from 'rxjs/operators';
@@ -78,7 +78,6 @@ export class AssetPreviewComponent implements OnInit, OnDestroy {
     }
 
     ngOnInit() {
-        const { focalPoint } = this.asset;
         if (this.assets?.length) {
             this.showSlideButtons = true;
             this.previewAssetIndex = this.assets.findIndex(asset => asset.id === this.asset.id) || 0;

+ 16 - 6
packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/prosemirror/inputrules.ts

@@ -60,21 +60,31 @@ export function headingRule(nodeType, maxLevel) {
 export function buildInputRules(schema: Schema): Plugin {
     const rules = smartQuotes.concat(ellipsis, emDash);
     let type: NodeType;
-    /* eslint-disable no-cond-assign */
-    if ((type = schema.nodes.blockquote)) {
+
+    type = schema.nodes.blockquote;
+    if (type) {
         rules.push(blockQuoteRule(type));
     }
-    if ((type = schema.nodes.ordered_list)) {
+
+    type = schema.nodes.ordered_list;
+    if (type) {
         rules.push(orderedListRule(type));
     }
-    if ((type = schema.nodes.bullet_list)) {
+
+    type = schema.nodes.bullet_list;
+    if (type) {
         rules.push(bulletListRule(type));
     }
-    if ((type = schema.nodes.code_block)) {
+
+    type = schema.nodes.code_block;
+    if (type) {
         rules.push(codeBlockRule(type));
     }
-    if ((type = schema.nodes.heading)) {
+
+    type = schema.nodes.heading;
+    if (type) {
         rules.push(headingRule(type, 6));
     }
+
     return inputRules({ rules });
 }

+ 34 - 13
packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/prosemirror/keymap.ts

@@ -74,29 +74,40 @@ export function buildKeymap(schema: Schema, mapKeys?: Keymap) {
     bind('Mod-BracketLeft', lift);
     bind('Escape', selectParentNode);
 
-    /* eslint-disable no-cond-assign */
-    if ((type = schema.marks.strong)) {
+    type = schema.marks.strong;
+    if (type) {
         bind('Mod-b', toggleMark(type));
         bind('Mod-B', toggleMark(type));
     }
-    if ((type = schema.marks.em)) {
+
+    type = schema.marks.em;
+    if (type) {
         bind('Mod-i', toggleMark(type));
         bind('Mod-I', toggleMark(type));
     }
-    if ((type = schema.marks.code)) {
+
+    type = schema.marks.code;
+    if (type) {
         bind('Mod-`', toggleMark(type));
     }
 
-    if ((type = schema.nodes.bullet_list)) {
+    type = schema.nodes.bullet_list;
+    if (type) {
         bind('Shift-Ctrl-8', wrapInList(type));
     }
-    if ((type = schema.nodes.ordered_list)) {
+
+    type = schema.nodes.ordered_list;
+    if (type) {
         bind('Shift-Ctrl-9', wrapInList(type));
     }
-    if ((type = schema.nodes.blockquote)) {
+
+    type = schema.nodes.blockquote;
+    if (type) {
         bind('Ctrl->', wrapIn(type));
     }
-    if ((type = schema.nodes.hard_break)) {
+
+    type = schema.nodes.hard_break;
+    if (type) {
         const br = type;
         const cmd = chainCommands(exitCode, (state, dispatch) => {
             // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -109,23 +120,33 @@ export function buildKeymap(schema: Schema, mapKeys?: Keymap) {
             bind('Ctrl-Enter', cmd);
         }
     }
-    if ((type = schema.nodes.list_item)) {
+
+    type = schema.nodes.list_item;
+    if (type) {
         bind('Enter', splitListItem(type));
         bind('Mod-[', liftListItem(type));
         bind('Mod-]', sinkListItem(type));
     }
-    if ((type = schema.nodes.paragraph)) {
+
+    type = schema.nodes.paragraph;
+    if (type) {
         bind('Shift-Ctrl-0', setBlockType(type));
     }
-    if ((type = schema.nodes.code_block)) {
+
+    type = schema.nodes.code_block;
+    if (type) {
         bind('Shift-Ctrl-\\', setBlockType(type));
     }
-    if ((type = schema.nodes.heading)) {
+
+    type = schema.nodes.heading;
+    if (type) {
         for (let i = 1; i <= 6; i++) {
             bind('Shift-Ctrl-' + i, setBlockType(type, { level: i }));
         }
     }
-    if ((type = schema.nodes.horizontal_rule)) {
+
+    type = schema.nodes.horizontal_rule;
+    if (type) {
         const hr = type;
         bind('Mod-_', (state, dispatch) => {
             dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView());

+ 35 - 13
packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/prosemirror/menu/menu.ts

@@ -124,60 +124,80 @@ function wrapListItem(nodeType, options: CmdItemOptions) {
 export function buildMenuItems(schema: Schema, modalService: ModalService) {
     const r: Record<string, any> = {};
     let type: MarkType | NodeType;
-    /* eslint-disable no-cond-assign */
-    if ((type = schema.marks.strong)) {
+
+    type = schema.marks.strong;
+    if (type) {
         r.toggleStrong = markItem(type, {
             title: 'Toggle strong style',
             iconShape: 'bold',
         });
     }
-    if ((type = schema.marks.em)) {
+
+    type = schema.marks.em;
+    if (type) {
         r.toggleEm = markItem(type, {
             title: 'Toggle emphasis',
             iconShape: 'italic',
         });
     }
-    if ((type = schema.marks.code)) {
+
+    type = schema.marks.code;
+    if (type) {
         r.toggleCode = markItem(type, { title: 'Toggle code font', icon: icons.code });
     }
-    if ((type = schema.marks.link)) {
+
+    type = schema.marks.link;
+    if (type) {
         r.toggleLink = linkItem(type, modalService);
     }
 
-    if ((type = schema.nodes.image)) {
+    type = schema.nodes.image;
+    if (type) {
         r.insertImage = insertImageItem(type, modalService);
     }
-    if ((type = schema.nodes.bullet_list)) {
+
+    type = schema.nodes.bullet_list;
+    if (type) {
         r.wrapBulletList = wrapListItem(type, {
             title: 'Wrap in bullet list',
             iconShape: 'bullet-list',
         });
     }
-    if ((type = schema.nodes.ordered_list)) {
+
+    type = schema.nodes.ordered_list;
+    if (type) {
         r.wrapOrderedList = wrapListItem(type, {
             title: 'Wrap in ordered list',
             iconShape: 'number-list',
         });
     }
-    if ((type = schema.nodes.blockquote)) {
+
+    type = schema.nodes.blockquote;
+    if (type) {
         r.wrapBlockQuote = wrapItem(type, {
             title: 'Wrap in block quote',
             render: renderClarityIcon({ shape: 'block-quote', size: IconSize.Large }),
         });
     }
-    if ((type = schema.nodes.paragraph)) {
+
+    type = schema.nodes.paragraph;
+    if (type) {
         r.makeParagraph = blockTypeItem(type, {
             title: 'Change to paragraph',
             render: renderClarityIcon({ shape: 'text', label: 'Plain' }),
         });
     }
-    if ((type = schema.nodes.code_block)) {
+
+    type = schema.nodes.code_block;
+    if (type) {
         r.makeCodeBlock = blockTypeItem(type, {
             title: 'Change to code block',
             render: renderClarityIcon({ shape: 'code', label: 'Code' }),
         });
     }
-    if ((type = schema.nodes.heading)) {
+
+    type = schema.nodes.heading;
+    if (type) {
         for (let i = 1; i <= 10; i++) {
             r['makeHead' + i] = blockTypeItem(type, {
                 title: 'Change to heading ' + i,
@@ -186,7 +206,9 @@ export function buildMenuItems(schema: Schema, modalService: ModalService) {
             });
         }
     }
-    if ((type = schema.nodes.horizontal_rule)) {
+
+    type = schema.nodes.horizontal_rule;
+    if (type) {
         const hr = type;
         r.insertHorizontalRule = new MenuItem({
             title: 'Insert horizontal rule',

+ 1 - 2
packages/admin-ui/src/lib/customer/src/components/customer-group-detail/customer-group-detail.component.ts

@@ -1,5 +1,5 @@
 import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
-import { FormBuilder, UntypedFormGroup } from '@angular/forms';
+import { FormBuilder } from '@angular/forms';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
 import { ResultOf } from '@graphql-typed-document-node/core';
 import {
@@ -107,7 +107,6 @@ export class CustomerGroupDetailComponent
         });
 
         if (this.customFields.length) {
-            const customFieldsGroup = this.detailForm.get(['customFields']) as UntypedFormGroup;
             this.setCustomFieldFormValues(this.customFields, this.detailForm.get('customFields'), entity);
         }
     }

+ 0 - 1
packages/admin-ui/src/lib/order/src/components/order-history/order-history.component.ts

@@ -164,7 +164,6 @@ export class OrderHistoryComponent {
         for (const line of this.order.lines) {
             const cancellationLine = cancellationLines.find(l => l.orderLineId === line.id);
             if (cancellationLine) {
-                const count = itemMap.get(line.productVariant.name);
                 itemMap.set(line.productVariant.name, cancellationLine.quantity);
             }
         }

+ 0 - 2
packages/admin-ui/src/lib/react/src/react-hooks/use-route-params.ts

@@ -33,8 +33,6 @@ export function useRouteParams() {
         return () => subscription.unsubscribe();
     }, []);
 
-    activatedRoute;
-
     return {
         params,
         queryParams,

+ 1 - 3
packages/admin-ui/src/lib/settings/src/components/shipping-method-detail/shipping-method-detail.component.ts

@@ -47,7 +47,7 @@ export const GET_SHIPPING_METHOD_DETAIL = gql`
     templateUrl: './shipping-method-detail.component.html',
     styleUrls: ['./shipping-method-detail.component.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush,
-    standalone: false
+    standalone: false,
 })
 export class ShippingMethodDetailComponent
     extends TypedBaseDetailComponent<typeof GetShippingMethodDetailDocument, 'shippingMethod'>
@@ -178,7 +178,6 @@ export class ShippingMethodDetailComponent
         if (!selectedChecker || !selectedCalculator || !checker || !calculator) {
             return;
         }
-        const formValue = this.detailForm.value;
         const input = {
             ...(this.getUpdatedShippingMethod(
                 {
@@ -227,7 +226,6 @@ export class ShippingMethodDetailComponent
             .pipe(
                 take(1),
                 mergeMap(([shippingMethod, languageCode]) => {
-                    const formValue = this.detailForm.value;
                     const input = {
                         ...(this.getUpdatedShippingMethod(
                             shippingMethod,

+ 4 - 5
packages/admin-ui/src/lib/settings/src/components/tax-rate-detail/tax-rate-detail.component.ts

@@ -34,7 +34,7 @@ export const GET_TAX_RATE_DETAIL = gql`
     templateUrl: './tax-rate-detail.component.html',
     styleUrls: ['./tax-rate-detail.component.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush,
-    standalone: false
+    standalone: false,
 })
 export class TaxRateDetailComponent
     extends TypedBaseDetailComponent<typeof GetTaxRateDetailDocument, 'taxRate'>
@@ -82,20 +82,19 @@ export class TaxRateDetailComponent
         if (!this.detailForm.dirty) {
             return;
         }
-        const { name, enabled, value, taxCategoryId, zoneId, customerGroupId, customFields } =
+        const { name, enabled, value, taxCategoryId, zoneId, customFields, customerGroupId } =
             this.detailForm.value;
         if (!name || enabled == null || value == null || !taxCategoryId || !zoneId) {
             return;
         }
-        const formValue = this.detailForm.value;
         const input = {
             name,
             enabled,
             value,
             categoryId: taxCategoryId,
             zoneId,
-            customerGroupId: formValue.customerGroupId,
-            customFields: formValue.customFields,
+            customerGroupId,
+            customFields,
         } satisfies CreateTaxRateInput;
         this.dataService.settings.createTaxRate(input).subscribe(
             data => {

+ 12 - 4
packages/asset-server-plugin/src/asset-server.ts

@@ -258,13 +258,21 @@ export class AssetServer {
             Logger.error((e.message as string) + ': ' + filePath, loggerCtx);
             return '';
         }
-        if (!(this.assetStorageStrategy instanceof S3AssetStorageStrategy)) {
+        if (this.assetStorageStrategy instanceof S3AssetStorageStrategy) {
             // For S3 storage, we don't need to sanitize the path because
             // directory traversal attacks are not possible, and modifying the
-            // path in this way can s3 files to be not found.
-            return path.normalize(decodedPath).replace(/(\.\.[\/\\])+/, '');
-        } else {
+            // path in this way can cause S3 files to be not found.
             return decodedPath;
+        } else {
+            // For local storage, we make sure to sanitize the path to prevent directory traversal attacks.
+            const normalizedPath = path.normalize(decodedPath);
+            let sanitizedPath = normalizedPath;
+            let previousPath;
+            do {
+                previousPath = sanitizedPath;
+                sanitizedPath = previousPath.replace(/(\.\.[\\/\\])+/g, '');
+            } while (sanitizedPath !== previousPath);
+            return sanitizedPath;
         }
     }
 

+ 25 - 0
packages/core/src/common/safe-assign.ts

@@ -0,0 +1,25 @@
+/**
+ * Safely assigns a property to an object, preventing prototype pollution.
+ *
+ * This function guards against prototype pollution by:
+ * 1. Blocking dangerous property names (__proto__, constructor, prototype)
+ * 2. Using Object.defineProperty for safer assignment instead of direct assignment
+ *
+ * @param target - The target object to assign the property to
+ * @param key - The property key to assign
+ * @param value - The value to assign to the property
+ */
+export function safeAssign(target: any, key: string, value: any): void {
+    // Guard against prototype pollution by blocking dangerous property names
+    if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
+        return;
+    }
+
+    // Use Object.defineProperty for safer assignment
+    Object.defineProperty(target, key, {
+        value,
+        writable: true,
+        enumerable: true,
+        configurable: true,
+    });
+}

+ 10 - 3
packages/core/src/config/merge-config.ts

@@ -1,6 +1,8 @@
 import { isClassInstance, isObject } from '@vendure/common/lib/shared-utils';
 import { simpleDeepClone } from '@vendure/common/lib/simple-deep-clone';
 
+import { safeAssign } from '../common/safe-assign';
+
 import { PartialVendureConfig, VendureConfig } from './vendure-config';
 
 /**
@@ -38,17 +40,22 @@ export function mergeConfig<T extends VendureConfig>(target: T, source: PartialV
 
     if (isObject(target) && isObject(source)) {
         for (const key in source) {
+            // Guard against prototype pollution - block dangerous property names
+            if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
+                continue;
+            }
+
             if (isObject((source as any)[key])) {
                 if (!(target as any)[key]) {
-                    Object.assign(target as any, { [key]: {} });
+                    safeAssign(target, key, {});
                 }
                 if (!isClassInstance((source as any)[key])) {
                     mergeConfig((target as any)[key], (source as any)[key], depth + 1);
                 } else {
-                    (target as any)[key] = (source as any)[key];
+                    safeAssign(target, key, (source as any)[key]);
                 }
             } else {
-                Object.assign(target, { [key]: (source as any)[key] });
+                safeAssign(target, key, (source as any)[key]);
             }
         }
     }

+ 11 - 5
packages/core/src/entity/validate-custom-fields-config.ts

@@ -52,11 +52,17 @@ function assertNoNameConflictsWithEntity(entity: Type<any>, customFields: Custom
 function assertNoDuplicatedCustomFieldNames(entityName: string, customFields: CustomFieldConfig[]): string[] {
     const nameCounts = customFields
         .map(f => f.name)
-        .reduce((hash, name) => {
-            // eslint-disable-next-line @typescript-eslint/no-unused-expressions
-            hash[name] ? hash[name]++ : (hash[name] = 1);
-            return hash;
-        }, {} as { [name: string]: number });
+        .reduce(
+            (hash, name) => {
+                if (hash[name]) {
+                    hash[name]++;
+                } else {
+                    hash[name] = 1;
+                }
+                return hash;
+            },
+            {} as { [name: string]: number },
+        );
     return Object.entries(nameCounts)
         .filter(([name, count]) => 1 < count)
         .map(([name, count]) => `${entityName} entity has duplicated custom field name: "${name}"`);

+ 20 - 4
packages/core/src/service/helpers/entity-hydrator/merge-deep.ts

@@ -1,5 +1,7 @@
 import { isObject } from '@vendure/common/lib/shared-utils';
 
+import { safeAssign } from '../../../common/safe-assign';
+
 /**
  * Merges properties into a target entity. This is needed for the cases in which a
  * property already exists on the target, but the hydrated version also contains that
@@ -28,8 +30,8 @@ export function mergeDeep<T extends { [key: string]: any }>(
             // If the array contains entities, we can use the id to match them up
             // so that we ensure that we don't merge properties from different entities
             // with the same index.
-            const aIds = a.map((e) => e.id);
-            const bIds = b.map((e) => e.id);
+            const aIds = a.map(e => e.id);
+            const bIds = b.map(e => e.id);
             if (JSON.stringify(aIds) !== JSON.stringify(bIds)) {
                 // The entities in the arrays are not in the same order, so we can't
                 // safely merge them. We need to sort the `b` array so that the entities
@@ -46,15 +48,29 @@ export function mergeDeep<T extends { [key: string]: any }>(
     }
 
     for (const [key, value] of Object.entries(b)) {
+        // Guard against prototype pollution - block dangerous property names
+        if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
+            continue;
+        }
+
         if (Object.getOwnPropertyDescriptor(b, key)?.writable) {
             if (Array.isArray(value) || isObject(value)) {
                 // Skip if we detect a circular reference
                 if (isObject(value) && visited.has(value)) {
                     continue;
                 }
-                (a as any)[key] = mergeDeep(a?.[key], b[key], visited);
+                // Only merge recursively if the property exists as an own property in the destination object
+                if (
+                    Object.prototype.hasOwnProperty.call(a, key) &&
+                    (Array.isArray(a[key]) || isObject(a[key]))
+                ) {
+                    const mergedValue = mergeDeep(a[key], b[key], visited);
+                    safeAssign(a, key, mergedValue);
+                } else {
+                    safeAssign(a, key, value);
+                }
             } else {
-                (a as any)[key] = b[key];
+                safeAssign(a, key, value);
             }
         }
     }

+ 0 - 1
packages/dashboard/src/app/routes/_authenticated/_orders/components/customer-address-selector.tsx

@@ -32,7 +32,6 @@ interface CustomerAddressSelectorProps {
 }
 
 export function CustomerAddressSelector({ customerId, onSelect }: Readonly<CustomerAddressSelectorProps>) {
-    const { i18n } = useLingui();
     const [open, setOpen] = useState(false);
 
     const { data, isLoading } = useQuery<CustomerAddressesQuery>({

+ 6 - 2
packages/dashboard/src/app/routes/_authenticated/_orders/components/payment-details.tsx

@@ -18,7 +18,11 @@ import {
     transitionPaymentToStateDocument,
 } from '../orders.graphql.js';
 import { SettleRefundDialog } from './settle-refund-dialog.js';
-import { getTypeForState, StateTransitionControl } from './state-transition-control.js';
+import {
+    getTypeForState,
+    StateTransitionAction,
+    StateTransitionControl,
+} from './state-transition-control.js';
 
 type PaymentDetailsProps = {
     payment: ResultOf<typeof paymentWithRefundsFragment>;
@@ -131,7 +135,7 @@ export function PaymentDetails({ payment, currencyCode, onSuccess }: Readonly<Pa
     };
 
     const getPaymentActions = () => {
-        const actions = [];
+        const actions: StateTransitionAction[] = [];
 
         if (payment.nextStates?.includes('Settled')) {
             actions.push({

+ 2 - 2
packages/dashboard/src/app/routes/_authenticated/_orders/components/state-transition-control.tsx

@@ -9,9 +9,9 @@ import { Trans } from '@/vdb/lib/trans.js';
 import { cn } from '@/vdb/lib/utils.js';
 import { EllipsisVertical, CircleDashed, CircleCheck, CircleX } from 'lucide-react';
 
-type StateType = 'default' | 'destructive' | 'success';
+export type StateType = 'default' | 'destructive' | 'success';
 
-type StateTransitionAction = {
+export type StateTransitionAction = {
     label: string;
     onClick: () => void;
     disabled?: boolean;

+ 0 - 1
packages/dashboard/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx

@@ -249,7 +249,6 @@ function DraftOrderPage() {
     const { mutate: removeCouponCodeForDraftOrder } = useMutation({
         mutationFn: api.mutate(removeCouponCodeFromDraftOrderDocument),
         onSuccess: (result: ResultOf<typeof removeCouponCodeFromDraftOrderDocument>) => {
-            const order = result.removeCouponCodeFromDraftOrder;
             toast.success(i18n.t('Coupon code removed from order'));
             refreshEntity();
         },

+ 1 - 1
packages/dashboard/src/app/routes/_authenticated/_products/components/option-value-input.tsx

@@ -37,7 +37,7 @@ export function OptionValueInput({
     groupIndex,
     disabled = false,
 }: Readonly<OptionValueInputProps>) {
-    const { control, watch } = useFormContext<FormValues>();
+    const { control } = useFormContext<FormValues>();
     const { fields, append, remove } = useFieldArray({
         control,
         name: `optionGroups.${groupIndex}.values`,

+ 0 - 7
packages/dashboard/src/app/routes/_authenticated/_zones/components/zone-countries-table.tsx

@@ -28,13 +28,6 @@ export function ZoneCountriesTable({ zoneId, canAddCountries = false }: Readonly
         },
     });
 
-    const { mutate: removeCountryFromZone } = useMutation({
-        mutationFn: api.mutate(removeCountryFromZoneMutation),
-        onSuccess: () => {
-            refetch();
-        },
-    });
-
     const [page, setPage] = useState(1);
     const [pageSize, setPageSize] = useState(10);
 

+ 1 - 1
packages/dashboard/src/lib/components/layout/content-language-selector.tsx

@@ -14,7 +14,7 @@ export function ContentLanguageSelector({ value, onChange, className }: ContentL
     const serverConfig = useServerConfig();
     const { formatLanguageName } = useLocalFormat();
     const {
-        settings: { contentLanguage, displayLanguage },
+        settings: { contentLanguage },
         setContentLanguage,
     } = useUserSettings();
 

+ 0 - 6
packages/dashboard/src/lib/components/shared/asset/asset-preview.tsx

@@ -29,12 +29,6 @@ export function AssetPreview({ asset, assets, customFields = [] }: Readonly<Asse
     const imageRef = useRef<HTMLImageElement>(null);
     const containerRef = useRef<HTMLDivElement>(null);
 
-    const form = useForm({
-        defaultValues: {
-            name: asset.name,
-            tags: asset.tags?.map(t => t.value) || [],
-        },
-    });
     const activeAsset = assets?.[assetIndex] ?? asset;
 
     useEffect(() => {

+ 1 - 1
packages/dashboard/src/lib/components/shared/option-value-input.tsx

@@ -32,7 +32,7 @@ interface OptionValueInputProps {
 }
 
 export function OptionValueInput({ groupIndex, disabled = false }: Readonly<OptionValueInputProps>) {
-    const { control, watch } = useFormContext<FormValues>();
+    const { control } = useFormContext<FormValues>();
     const { fields, append, remove } = useFieldArray({
         control,
         name: `optionGroups.${groupIndex}.values`,

+ 1 - 1
packages/dashboard/src/lib/components/shared/product-variant-selector.tsx

@@ -59,7 +59,7 @@ export function ProductVariantSelector({ onProductVariantSelect }: Readonly<Prod
     const [open, setOpen] = useState(false);
     const debouncedSearch = useDebounce(search, 500);
 
-    const { data, isLoading } = useQuery({
+    const { data } = useQuery({
         queryKey: ['productVariants', debouncedSearch],
         staleTime: 1000 * 60 * 5,
         enabled: debouncedSearch.length > 0,

+ 1 - 1
packages/dashboard/src/lib/components/ui/calendar.tsx

@@ -77,7 +77,7 @@ function Calendar({
         }, [yearRange]),
     );
 
-    const { onNextClick, onPrevClick, startMonth, endMonth } = props;
+    const { onPrevClick, startMonth, endMonth } = props;
 
     const columnsDisplayed = navView === 'years' ? 1 : numberOfMonths;
 

+ 1 - 1
packages/dashboard/src/lib/framework/dashboard-widget/metrics-widget/index.tsx

@@ -19,7 +19,7 @@ export function MetricsWidget() {
     const { activeChannel } = useChannel();
     const [dataType, setDataType] = useState<DATA_TYPES>(DATA_TYPES.OrderTotal);
 
-    const { data, error } = useQuery({
+    const { data } = useQuery({
         queryKey: ['dashboard-order-metrics', dataType],
         queryFn: () => {
             return api.query(orderChartDataQuery, {

+ 0 - 2
packages/dashboard/src/lib/framework/dashboard-widget/orders-summary/index.tsx

@@ -37,8 +37,6 @@ function PercentageChange({ value }: PercentageChangeProps) {
 
 export function OrdersSummaryWidget() {
     const [range, setRange] = useState<Range>(Range.Today);
-    const { formatCurrency } = useLocalFormat();
-    const { activeChannel } = useChannel();
 
     const variables = useMemo(() => {
         const now = new Date();

+ 32 - 30
packages/dashboard/src/lib/hooks/use-extended-list-query.ts

@@ -10,14 +10,13 @@ import { usePage } from './use-page.js';
 
 export function useExtendedListQuery<T extends DocumentNode>(listQuery: T) {
     const { pageId } = usePage();
-    const { blockId } = usePageBlock();
+    const { blockId } = usePageBlock() ?? {};
     const { i18n } = useLingui();
     const listQueryExtensions = pageId && blockId ? getListQueryDocuments(pageId, blockId) : [];
     const hasShownError = useRef(false);
 
     const extendedListQuery = useMemo(() => {
         let result: T = listQuery;
-        let error: Error | null = null;
 
         try {
             result = listQueryExtensions.reduce(
@@ -25,40 +24,43 @@ export function useExtendedListQuery<T extends DocumentNode>(listQuery: T) {
                 listQuery,
             ) as T;
         } catch (err) {
-            error = err instanceof Error ? err : new Error(String(err));
+            const error = err instanceof Error ? err : new Error(String(err));
             // Continue with the original query instead of the extended one
             result = listQuery;
-        }
 
-        // Store error for useEffect to handle
-        if (error && !hasShownError.current) {
-            hasShownError.current = true;
+            // Store error for useEffect to handle
+            if (error && !hasShownError.current) {
+                hasShownError.current = true;
 
-            // Provide a helpful error message based on the error type
-            let errorMessage = i18n.t('Failed to extend query document');
-            if (error.message.includes('Extension query must have at least one top-level field')) {
-                errorMessage = i18n.t('Query extension is invalid: must have at least one top-level field');
-            } else if (error.message.includes('The query extension must extend the')) {
-                errorMessage = i18n.t('Query extension mismatch: ') + error.message;
-            } else if (error.message.includes('Syntax Error')) {
-                errorMessage = i18n.t('Query extension contains invalid GraphQL syntax');
-            } else {
-                errorMessage = i18n.t('Query extension error: ') + error.message;
-            }
+                // Provide a helpful error message based on the error type
+                let errorMessageText = i18n.t('Failed to extend query document');
+                if (error.message.includes('Extension query must have at least one top-level field')) {
+                    errorMessageText = i18n.t(
+                        'Query extension is invalid: must have at least one top-level field',
+                    );
+                } else if (error.message.includes('The query extension must extend the')) {
+                    errorMessageText = i18n.t('Query extension mismatch: ') + error.message;
+                } else if (error.message.includes('Syntax Error')) {
+                    errorMessageText = i18n.t('Query extension contains invalid GraphQL syntax');
+                } else {
+                    errorMessageText = i18n.t('Query extension error: ') + error.message;
+                }
 
-            // Log the error and continue with the original query
-            // eslint-disable-next-line no-console
-            console.warn(`${errorMessage}. Continuing with original query.`, {
-                pageId,
-                blockId,
-                extensionsCount: listQueryExtensions.length,
-                error: error.message,
-            });
+                // Log the error and continue with the original query
+                // eslint-disable-next-line no-console
+                console.warn(`${errorMessageText}. Continuing with original query.`, {
+                    pageId,
+                    blockId,
+                    extensionsCount: listQueryExtensions.length,
+                    error: error.message,
+                });
 
-            // Show a user-friendly toast notification
-            toast.error(i18n.t('Query extension error'), {
-                description: errorMessage + '. ' + i18n.t('The page will continue with the default query.'),
-            });
+                // Show a user-friendly toast notification
+                toast.error(i18n.t('Query extension error'), {
+                    description:
+                        errorMessageText + '. ' + i18n.t('The page will continue with the default query.'),
+                });
+            }
         }
 
         return result;

+ 2 - 2
packages/dashboard/vite/utils/config-loader.ts

@@ -372,7 +372,7 @@ export async function compileFile({
                 // Attempt to resolve using path aliases
                 let resolved = false;
                 for (const [alias, patterns] of Object.entries(tsConfigInfo.paths)) {
-                    const aliasPrefix = alias.replace('*', '');
+                    const aliasPrefix = alias.replace(/\*/g, '');
                     const aliasSuffix = alias.endsWith('*') ? '*' : '';
 
                     if (
@@ -381,7 +381,7 @@ export async function compileFile({
                     ) {
                         const remainingImportPath = importPath.slice(aliasPrefix.length);
                         for (const pattern of patterns) {
-                            const patternPrefix = pattern.replace('*', '');
+                            const patternPrefix = pattern.replace(/\*/g, '');
                             const patternSuffix = pattern.endsWith('*') ? '*' : '';
                             // Ensure suffix match consistency (* vs exact)
                             if (aliasSuffix !== patternSuffix) continue;

+ 0 - 1
packages/email-plugin/src/plugin.ts

@@ -407,7 +407,6 @@ export class EmailPlugin implements OnApplicationBootstrap, OnApplicationShutdow
         event: EventWithContext,
     ) {
         Logger.debug(`Handling event "${handler.type}"`, loggerCtx);
-        const { type } = handler;
         try {
             const injector = new Injector(this.moduleRef);
             let globalTemplateVars = this.options.globalTemplateVars;

+ 11 - 12
scripts/codegen/plugins/graphql-errors-plugin.ts

@@ -2,8 +2,6 @@ import { PluginFunction } from '@graphql-codegen/plugin-helpers';
 import { buildScalars } from '@graphql-codegen/visitor-plugin-common';
 import {
     ASTNode,
-    ASTVisitor,
-    FieldDefinitionNode,
     getNamedType,
     GraphQLFieldMap,
     GraphQLNamedType,
@@ -11,18 +9,13 @@ import {
     GraphQLSchema,
     GraphQLType,
     GraphQLUnionType,
-    InterfaceTypeDefinitionNode,
-    isNamedType,
     isObjectType,
     isTypeDefinitionNode,
     isUnionType,
     Kind,
-    ListTypeNode,
-    NonNullTypeNode,
     ObjectTypeDefinitionNode,
     parse,
     printSchema,
-    UnionTypeDefinitionNode,
     visit,
 } from 'graphql';
 import { ASTVisitFn } from 'graphql/language/visitor';
@@ -44,8 +37,8 @@ const errorsVisitor: ASTVisitFn<ASTNode> = (node, key, parent) => {
             return node.type.kind === 'NamedType'
                 ? node.type.name.value
                 : node.type.kind === 'ListType'
-                ? node.type
-                : '';
+                  ? node.type
+                  : '';
         }
         case Kind.FIELD_DEFINITION: {
             const type = (node.type.kind === 'ListType' ? node.type.type : node.type) as unknown as string;
@@ -177,11 +170,17 @@ function generateTypeResolvers(schema: GraphQLSchema) {
         if (!typesHandled.has(returnType.name)) {
             typesHandled.add(returnType.name);
             const nonErrorResult = returnType.getTypes().find(t => !inheritsFromErrorResult(t));
+            if (!nonErrorResult) {
+                throw new Error(
+                    `The type "${returnType.name}" seems to be a union of only ErrorResult types. ` +
+                        `This is not a valid union in Vendure, as it should also contain a non-error result type.`,
+                );
+            }
             result.push(
                 `  ${returnType.name}: {`,
                 `    __resolveType(value: any) {`,
                 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-                `      return isGraphQLError(value) ? (value as any).__typename : '${nonErrorResult!.name}';`,
+                `      return isGraphQLError(value) ? (value as any).__typename : '${nonErrorResult.name}';`,
                 `    },`,
                 `  },`,
             );
@@ -201,7 +200,7 @@ function getOperationsThatReturnErrorUnions(schema: GraphQLSchema, fields: Graph
     });
 }
 
-function isUnionOfResultAndErrors(schema: GraphQLSchema, types: ReadonlyArray<GraphQLObjectType>) {
+function isUnionOfResultAndErrors(schema: GraphQLSchema, types: readonly GraphQLObjectType[]) {
     const errorResultTypes = types.filter(type => {
         if (isObjectType(type)) {
             if (inheritsFromErrorResult(type)) {
@@ -210,7 +209,7 @@ function isUnionOfResultAndErrors(schema: GraphQLSchema, types: ReadonlyArray<Gr
         }
         return false;
     });
-    return (errorResultTypes.length = types.length - 1);
+    return errorResultTypes.length === types.length - 1;
 }
 
 function isObjectTypeDefinition(node: any): node is ObjectTypeDefinitionNode {