Skip to content

Commit ef405ab

Browse files
authored
Merge pull request #901 from contentstack/dev
Dev
2 parents 69bb4a2 + e5afce2 commit ef405ab

11 files changed

Lines changed: 560 additions & 578 deletions

File tree

api/src/services/contentMapper.service.ts

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,26 @@ const putTestData = async (req: Request) => {
6060
if (item?.advanced) {
6161
item.advanced.initial = structuredClone(item?.advanced);
6262
}
63+
if(item?.refrenceTo) {
64+
item.initialRefrenceTo = item?.refrenceTo;
65+
}
6366
});
6467
});
6568

6669

6770

71+
const sanitizeObject = (obj: Record<string, any>) => {
72+
const blockedKeys = ['__proto__', 'prototype', 'constructor'];
73+
const safeObj: Record<string, any> = {};
74+
75+
for (const key in obj) {
76+
if (!blockedKeys.includes(key)) {
77+
safeObj[key] = obj[key];
78+
}
79+
}
80+
return safeObj;
81+
};
82+
6883
/*
6984
this code snippet iterates over an array of contentTypes and performs
7085
some operations on each element.
@@ -75,18 +90,38 @@ const putTestData = async (req: Request) => {
7590
Finally, it updates the fieldMapping property of each type in the contentTypes array with the fieldIds array.
7691
*/
7792
await FieldMapperModel.read();
78-
contentTypes.map((type: any, index: any) => {
93+
contentTypes.forEach((type: any, index: number) => {
7994
const fieldIds: string[] = [];
80-
const fields = Array?.isArray?.(type?.fieldMapping) ? type?.fieldMapping?.filter((field: any) => field)?.map?.((field: any) => {
81-
const id = field?.id ? field?.id?.replace(/[{}]/g, "")?.toLowerCase() : uuidv4();
82-
field.id = id;
83-
fieldIds.push(id);
84-
return { id, projectId, contentTypeId: type?.id, isDeleted: false, ...field };
85-
}) : [];
86-
95+
96+
const fields = Array.isArray(type?.fieldMapping) ?
97+
type.fieldMapping
98+
.filter(Boolean)
99+
.map((field: any) => {
100+
const safeField = sanitizeObject(field);
101+
102+
const id =
103+
safeField?.id ?
104+
safeField.id.replace(/[{}]/g, '').toLowerCase()
105+
: uuidv4();
106+
107+
fieldIds.push(id);
108+
109+
return {
110+
id,
111+
projectId,
112+
contentTypeId: type?.id,
113+
isDeleted: false,
114+
...safeField,
115+
};
116+
})
117+
: [];
118+
87119
FieldMapperModel.update((data: any) => {
88-
data.field_mapper = [...(data?.field_mapper ?? []), ...(fields ?? [])];
89-
});
120+
data.field_mapper = [
121+
...(Array.isArray(data?.field_mapper) ? data.field_mapper : []),
122+
...fields,
123+
];
124+
});
90125
if (
91126
Array?.isArray?.(contentType) &&
92127
Number?.isInteger?.(index) &&
@@ -277,8 +312,7 @@ const getFieldMapping = async (req: Request) => {
277312

278313
const fieldMapping: any = fieldData?.map((field: any) => {
279314
if (field?.advanced?.initial) {
280-
const { initial, ...restAdvanced } = field?.advanced ?? {};
281-
return { ...field, advanced: restAdvanced };
315+
return { ...field, advanced: field?.advanced };
282316
}
283317
return field;
284318
});
@@ -784,6 +818,9 @@ const resetToInitialMapping = async (req: Request) => {
784818
...field?.advanced?.initial,
785819
initial: field?.advanced?.initial,
786820
},
821+
...(field?.referenceTo && {
822+
referenceTo: field?.initialRefrenceTo
823+
}),
787824
isDeleted: false,
788825
}
789826
});

api/src/services/globalField.service.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,19 @@ const createGlobalField = async ({
5656
}
5757
}
5858

59+
const safeFileGlobalFields = fileGlobalFields;
60+
61+
const existingUids = new Set(
62+
safeFileGlobalFields?.map?.((gf: { uid: string }) => gf?.uid)
63+
);
64+
5965
const mergedGlobalFields = [
60-
...globalfields,
61-
...(fileGlobalFields?.filter(
62-
(fileField: { uid: string }) =>
63-
!globalfields?.some((gf: { uid: string }) => gf?.uid === fileField?.uid)
64-
) || [])
66+
...globalfields.filter(
67+
(fileField: { uid: string }) => !existingUids?.has(fileField?.uid)
68+
),
69+
...safeFileGlobalFields,
6570
];
71+
6672
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
6773
await fs.promises.writeFile(filePath, JSON.stringify(mergedGlobalFields, null, 2));
6874

api/src/utils/content-type-creator.utils.ts

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -964,23 +964,42 @@ const writeGlobalField = async (schema: any, globalSave: string) => {
964964
}
965965
};
966966

967-
const existingCtMapper = async ({ keyMapper, contentTypeUid, projectId, region, user_id }: any) => {
967+
const existingCtMapper = async ({ keyMapper, contentTypeUid, projectId, region, user_id, type}: any) => {
968968
try {
969969
const ctUid = keyMapper?.[contentTypeUid];
970-
const req: any = {
971-
params: {
972-
projectId,
973-
contentTypeUid: ctUid
974-
},
975-
body: {
976-
token_payload: {
977-
region,
978-
user_id
970+
971+
if(type === 'global_field') {
972+
973+
const req: any = {
974+
params: {
975+
projectId,
976+
globalFieldUid: ctUid
977+
},
978+
body: {
979+
token_payload: {
980+
region,
981+
user_id
982+
}
983+
}
984+
}
985+
const contentTypeSchema = await contentMapperService.getSingleGlobalField(req);
986+
return contentTypeSchema ?? null;
987+
} else {
988+
const req: any = {
989+
params: {
990+
projectId,
991+
contentTypeUid: ctUid
992+
},
993+
body: {
994+
token_payload: {
995+
region,
996+
user_id
997+
}
979998
}
980999
}
1000+
const contentTypeSchema = await contentMapperService.getExistingContentTypes(req);
1001+
return contentTypeSchema?.selectedContentType ?? null;
9811002
}
982-
const contentTypeSchema = await contentMapperService.getExistingContentTypes(req);
983-
return contentTypeSchema?.selectedContentType;
9841003
} catch (err) {
9851004
console.error("Error while getting the existing contentType from contenstack", err)
9861005
return {};
@@ -1042,7 +1061,7 @@ export const contenTypeMaker = async ({ contentType, destinationStackId, project
10421061
if (Object?.keys?.(keyMapper)?.length &&
10431062
keyMapper?.[contentType?.contentstackUid] !== "" &&
10441063
keyMapper?.[contentType?.contentstackUid] !== undefined) {
1045-
currentCt = await existingCtMapper({ keyMapper, contentTypeUid: contentType?.contentstackUid, projectId, region, user_id });
1064+
currentCt = await existingCtMapper({ keyMapper, contentTypeUid: contentType?.contentstackUid, projectId, region, user_id , type: contentType?.type});
10461065
}
10471066

10481067
// Safe: ensures we never pass undefined to the builder

ui/src/components/ContentMapper/contentMapper.interface.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export interface FieldMapType {
6868
_invalid?: boolean;
6969
backupFieldUid: string;
7070
refrenceTo: string[];
71+
initialRefrenceTo: string[];
7172
}
7273

7374
export interface Advanced {

ui/src/components/ContentMapper/index.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2012,9 +2012,9 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref:
20122012
contentstackFieldType: row?.backupFieldType,
20132013
contentstackField: row?.otherCmsField,
20142014
contentstackFieldUid: row?.backupFieldUid,
2015-
advanced: {
2016-
...row?.advanced?.initial,
2017-
},
2015+
advanced: row?.advanced?.initial,
2016+
...(row?.refrenceTo && { refrenceTo: row?.initialRefrenceTo }),
2017+
20182018
};
20192019
});
20202020
setTableData(updatedRows);
Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,43 @@
1-
import { useEffect } from 'react';
2-
import { useNavigate } from 'react-router-dom';
1+
import { useEffect, useRef } from 'react';
2+
import { useNavigate, useLocation } from 'react-router-dom';
3+
import { getSafeRouterPath } from '../utilities/functions';
34

5+
/**
6+
* Custom hook to prevent browser back navigation.
7+
* Uses React Router's internal location state instead of window.location
8+
* to avoid Open Redirect vulnerabilities (CWE-601).
9+
*/
410
const usePreventBackNavigation = (): void => {
511
const navigate = useNavigate();
12+
const location = useLocation();
13+
14+
// Store the current safe path from React Router's internal state
15+
// This avoids using window.location which is user-controlled
16+
const safePathRef = useRef<string>('/');
617

718
useEffect(() => {
19+
// Build the full path from React Router's location object
20+
// This is safe because React Router validates routes internally
21+
const fullPath = getSafeRouterPath(location, true);
22+
23+
// Store the validated path
24+
safePathRef.current = fullPath;
25+
26+
// Push a new history state to enable back navigation detection
27+
window.history.pushState({ preventBack: true }, '', fullPath);
28+
829
const handleBackNavigation = (event: PopStateEvent) => {
930
event.preventDefault();
10-
navigate(window.location.pathname, { replace: true });
31+
// Use the stored safe path from React Router, not window.location
32+
// Navigate to the path we stored from React Router's validated state
33+
window.history.pushState({ preventBack: true }, '', safePathRef.current);
1134
};
1235

13-
window.history.pushState(null, '', window.location.href);
14-
1536
window.addEventListener('popstate', handleBackNavigation);
1637

1738
return () => {
1839
window.removeEventListener('popstate', handleBackNavigation);
1940
};
20-
}, [navigate]);
41+
}, [navigate, location]);
2142
};
2243
export default usePreventBackNavigation;

ui/src/hooks/userNavigation.tsx

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,53 @@
1-
import { useEffect, useRef } from 'react';
2-
import { useLocation, useNavigate } from 'react-router-dom';
1+
import { useEffect, useRef, useCallback } from 'react';
2+
import { useLocation } from 'react-router-dom';
3+
import { getSafeRouterPath } from '../utilities/functions';
34

5+
/**
6+
* Custom hook to block browser navigation when a modal is open.
7+
* Uses stored pathname from React Router to avoid Open Redirect vulnerabilities (CWE-601).
8+
*/
49
const useBlockNavigation = (isModalOpen: boolean) => {
510
const location = useLocation();
6-
const navigate = useNavigate();
7-
const initialPathnameRef = useRef(location.pathname);
11+
12+
// Store the validated pathname when modal state changes
13+
// This breaks the data flow from user-controlled input to redirect
14+
const storedPathnameRef = useRef<string>('/');
15+
16+
// Memoized function to get the safe stored path
17+
const getSafeStoredPath = useCallback(() => {
18+
return storedPathnameRef.current;
19+
}, []);
20+
21+
// Update stored pathname only when modal is not open
22+
// This captures the safe path before any manipulation
23+
useEffect(() => {
24+
if (!isModalOpen) {
25+
// Store the current path from React Router's validated state
26+
storedPathnameRef.current = getSafeRouterPath(location);
27+
}
28+
}, [isModalOpen, location]);
829

930
useEffect(() => {
10-
const handlePopState = (event: PopStateEvent) => {
11-
// If the modal is open, prevent navigation
31+
const handlePopState = () => {
32+
// If the modal is open, prevent navigation by pushing state with stored safe path
1233
if (isModalOpen) {
13-
window.history.pushState(null, '', window.location.pathname);
14-
navigate(location.pathname);
34+
const safePath = getSafeStoredPath();
35+
window.history.pushState({ blockNav: true }, '', safePath);
1536
}
1637
};
1738

1839
if (isModalOpen) {
19-
initialPathnameRef.current = location.pathname;
20-
window.history.pushState(null, '', window.location.pathname);
40+
// Store the current safe path when modal opens
41+
storedPathnameRef.current = getSafeRouterPath(location);
42+
const safePath = getSafeStoredPath();
43+
window.history.pushState({ blockNav: true }, '', safePath);
2144
window.addEventListener('popstate', handlePopState);
2245
}
2346

2447
return () => {
2548
window.removeEventListener('popstate', handlePopState);
2649
};
27-
}, [isModalOpen, navigate, location.pathname]);
28-
29-
useEffect(() => {
30-
if (!isModalOpen) {
31-
initialPathnameRef.current = location.pathname;
32-
}
33-
}, [isModalOpen, location.pathname]);
50+
}, [isModalOpen, getSafeStoredPath, location]);
3451
};
3552

3653
export default useBlockNavigation;

ui/src/utilities/constants.interface.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,13 @@ export interface Image {
99
export interface MigrationStatesValues {
1010
[key: string]: boolean;
1111
}
12+
13+
/**
14+
* Interface representing React Router's location object structure.
15+
* Used for safe path extraction utilities.
16+
*/
17+
export interface RouterLocation {
18+
pathname?: string;
19+
search?: string;
20+
hash?: string;
21+
}

ui/src/utilities/functions.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Notification } from '@contentstack/venus-components';
22
import { WEBSITE_BASE_URL } from './constants';
3-
import { Image, ObjectType, MigrationStatesValues } from './constants.interface';
3+
import { Image, ObjectType, MigrationStatesValues, RouterLocation } from './constants.interface';
44

55
export const Locales = {
66
en: 'en-us',
@@ -179,3 +179,30 @@ export const getFileExtension = (filePath: string): string => {
179179
const validExtensionRegex = /\.(pdf|zip|xml|json)$/i;
180180
return ext && validExtensionRegex?.test(`.${ext}`) ? `${ext}` : '';
181181
};
182+
183+
/**
184+
* Extracts a safe path from React Router's location object.
185+
* Uses React Router's internal validated state instead of window.location
186+
* to avoid Open Redirect vulnerabilities (CWE-601).
187+
*
188+
* @param location - React Router's location object containing pathname, search, and hash
189+
* @param includeSearchAndHash - If true, includes search params and hash in the path. Default: false
190+
* @returns A safe path string, defaulting to '/' if no valid path is found
191+
*/
192+
export const getSafeRouterPath = (
193+
location: RouterLocation,
194+
includeSearchAndHash = false
195+
): string => {
196+
// Extract pathname from React Router's validated location state
197+
const pathname = location?.pathname || '/';
198+
199+
if (!includeSearchAndHash) {
200+
return pathname;
201+
}
202+
203+
// Build full path including search and hash when needed
204+
const search = location?.search || '';
205+
const hash = location?.hash || '';
206+
207+
return pathname + search + hash;
208+
};

0 commit comments

Comments
 (0)