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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 43 additions & 11 deletions api/src/services/contentMapper.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,26 @@ const putTestData = async (req: Request) => {
if (item?.advanced) {
item.advanced.initial = structuredClone(item?.advanced);
}
if( item?.refrenceTo) {
if(item?.refrenceTo) {
item.initialRefrenceTo = item?.refrenceTo;
}
});
});



const sanitizeObject = (obj: Record<string, any>) => {
const blockedKeys = ['__proto__', 'prototype', 'constructor'];
const safeObj: Record<string, any> = {};

for (const key in obj) {
if (!blockedKeys.includes(key)) {
safeObj[key] = obj[key];
}
}
return safeObj;
};

/*
this code snippet iterates over an array of contentTypes and performs
some operations on each element.
Expand All @@ -78,18 +90,38 @@ const putTestData = async (req: Request) => {
Finally, it updates the fieldMapping property of each type in the contentTypes array with the fieldIds array.
*/
await FieldMapperModel.read();
contentTypes.map((type: any, index: any) => {
contentTypes.forEach((type: any, index: number) => {
const fieldIds: string[] = [];
const fields = Array?.isArray?.(type?.fieldMapping) ? type?.fieldMapping?.filter((field: any) => field)?.map?.((field: any) => {
const id = field?.id ? field?.id?.replace(/[{}]/g, "")?.toLowerCase() : uuidv4();
field.id = id;
fieldIds.push(id);
return { id, projectId, contentTypeId: type?.id, isDeleted: false, ...field };
}) : [];


const fields = Array.isArray(type?.fieldMapping) ?
type.fieldMapping
.filter(Boolean)
.map((field: any) => {
const safeField = sanitizeObject(field);

const id =
safeField?.id ?
safeField.id.replace(/[{}]/g, '').toLowerCase()
: uuidv4();

fieldIds.push(id);

return {
id,
projectId,
contentTypeId: type?.id,
isDeleted: false,
...safeField,
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

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

Security vulnerability: The spread operator on line 114 reintroduces the prototype pollution risk by spreading the original unsanitized safeField object. After sanitizing with sanitizeObject, you're spreading safeField which could still contain malicious properties. The sanitizeObject call on line 100 creates a safe copy, but then spreading it allows all properties (including dangerous ones) to be included. Remove the spread operator and only use the explicitly defined properties.

Suggested change
...safeField,

Copilot uses AI. Check for mistakes.
};
})
: [];

FieldMapperModel.update((data: any) => {
data.field_mapper = [...(data?.field_mapper ?? []), ...(fields ?? [])];
});
data.field_mapper = [
...(Array.isArray(data?.field_mapper) ? data.field_mapper : []),
...fields,
];
});
if (
Array?.isArray?.(contentType) &&
Number?.isInteger?.(index) &&
Expand Down
33 changes: 27 additions & 6 deletions ui/src/hooks/usePreventBackNavigation.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,43 @@
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useEffect, useRef } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { getSafeRouterPath } from '../utilities/functions';

/**
* Custom hook to prevent browser back navigation.
* Uses React Router's internal location state instead of window.location
* to avoid Open Redirect vulnerabilities (CWE-601).
*/
const usePreventBackNavigation = (): void => {
const navigate = useNavigate();
const location = useLocation();

// Store the current safe path from React Router's internal state
// This avoids using window.location which is user-controlled
const safePathRef = useRef<string>('/');

useEffect(() => {
// Build the full path from React Router's location object
// This is safe because React Router validates routes internally
const fullPath = getSafeRouterPath(location, true);

// Store the validated path
safePathRef.current = fullPath;

// Push a new history state to enable back navigation detection
window.history.pushState({ preventBack: true }, '', fullPath);

const handleBackNavigation = (event: PopStateEvent) => {
event.preventDefault();
navigate(window.location.pathname, { replace: true });
// Use the stored safe path from React Router, not window.location
// Navigate to the path we stored from React Router's validated state
window.history.pushState({ preventBack: true }, '', safePathRef.current);
};

window.history.pushState(null, '', window.location.href);

window.addEventListener('popstate', handleBackNavigation);

return () => {
window.removeEventListener('popstate', handleBackNavigation);
};
}, [navigate]);
}, [navigate, location]);
};
export default usePreventBackNavigation;
51 changes: 34 additions & 17 deletions ui/src/hooks/userNavigation.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,53 @@
import { useEffect, useRef } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { useEffect, useRef, useCallback } from 'react';
import { useLocation } from 'react-router-dom';
import { getSafeRouterPath } from '../utilities/functions';

/**
* Custom hook to block browser navigation when a modal is open.
* Uses stored pathname from React Router to avoid Open Redirect vulnerabilities (CWE-601).
*/
const useBlockNavigation = (isModalOpen: boolean) => {
const location = useLocation();
const navigate = useNavigate();
const initialPathnameRef = useRef(location.pathname);

// Store the validated pathname when modal state changes
// This breaks the data flow from user-controlled input to redirect
const storedPathnameRef = useRef<string>('/');

// Memoized function to get the safe stored path
const getSafeStoredPath = useCallback(() => {
return storedPathnameRef.current;
}, []);

// Update stored pathname only when modal is not open
// This captures the safe path before any manipulation
useEffect(() => {
if (!isModalOpen) {
// Store the current path from React Router's validated state
storedPathnameRef.current = getSafeRouterPath(location);
}
}, [isModalOpen, location]);

useEffect(() => {
const handlePopState = (event: PopStateEvent) => {
// If the modal is open, prevent navigation
const handlePopState = () => {
// If the modal is open, prevent navigation by pushing state with stored safe path
if (isModalOpen) {
window.history.pushState(null, '', window.location.pathname);
navigate(location.pathname);
const safePath = getSafeStoredPath();
window.history.pushState({ blockNav: true }, '', safePath);
}
};

if (isModalOpen) {
initialPathnameRef.current = location.pathname;
window.history.pushState(null, '', window.location.pathname);
// Store the current safe path when modal opens
storedPathnameRef.current = getSafeRouterPath(location);
const safePath = getSafeStoredPath();
window.history.pushState({ blockNav: true }, '', safePath);
window.addEventListener('popstate', handlePopState);
}

return () => {
window.removeEventListener('popstate', handlePopState);
};
}, [isModalOpen, navigate, location.pathname]);

useEffect(() => {
if (!isModalOpen) {
initialPathnameRef.current = location.pathname;
}
}, [isModalOpen, location.pathname]);
}, [isModalOpen, getSafeStoredPath, location]);
};

export default useBlockNavigation;
10 changes: 10 additions & 0 deletions ui/src/utilities/constants.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,13 @@ export interface Image {
export interface MigrationStatesValues {
[key: string]: boolean;
}

/**
* Interface representing React Router's location object structure.
* Used for safe path extraction utilities.
*/
export interface RouterLocation {
pathname?: string;
search?: string;
hash?: string;
}
29 changes: 28 additions & 1 deletion ui/src/utilities/functions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Notification } from '@contentstack/venus-components';
import { WEBSITE_BASE_URL } from './constants';
import { Image, ObjectType, MigrationStatesValues } from './constants.interface';
import { Image, ObjectType, MigrationStatesValues, RouterLocation } from './constants.interface';

export const Locales = {
en: 'en-us',
Expand Down Expand Up @@ -179,3 +179,30 @@ export const getFileExtension = (filePath: string): string => {
const validExtensionRegex = /\.(pdf|zip|xml|json)$/i;
return ext && validExtensionRegex?.test(`.${ext}`) ? `${ext}` : '';
};

/**
* Extracts a safe path from React Router's location object.
* Uses React Router's internal validated state instead of window.location
* to avoid Open Redirect vulnerabilities (CWE-601).
*
* @param location - React Router's location object containing pathname, search, and hash
* @param includeSearchAndHash - If true, includes search params and hash in the path. Default: false
* @returns A safe path string, defaulting to '/' if no valid path is found
*/
export const getSafeRouterPath = (
location: RouterLocation,
includeSearchAndHash = false
): string => {
// Extract pathname from React Router's validated location state
const pathname = location?.pathname || '/';

if (!includeSearchAndHash) {
return pathname;
}

// Build full path including search and hash when needed
const search = location?.search || '';
const hash = location?.hash || '';

return pathname + search + hash;
};
Loading