Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "patch",
"comment": "fix: prevent prototype pollution in SankeyChart node ID maps.",
"packageName": "@fluentui/react-charting",
"email": "paulmardling@microsoft.com"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "patch",
"comment": "fix: prevent prototype pollution in SankeyChart node ID maps.",
"packageName": "@fluentui/react-charts",
"email": "paulmardling@microsoft.com"
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ type NodeValues = ItemValues<number>;
type LinkItemValues<T> = { [key: NodeId]: ItemValues<T> };
type LinkValues = LinkItemValues<number>;

type NodesInColumns = { [key: number]: SNode[] };
type NodesInColumns = ItemValues<SNode[]>;
type NormalizedData = ISankeyChartData & {
width: number;
height: number;
Expand All @@ -56,6 +56,10 @@ type NormalizeDiagramFunction = (
type NodeColors = { fillColor: string; borderColor: string };
type SankeyLayoutGenerator = SankeyLayout<SankeyGraph<{}, {}>, {}, {}>;

function createNodeIdMap<T>(): ItemValues<T> {
return Object.setPrototypeOf({}, null);
}

export interface ISankeyChartState extends IBasestate, IChartHoverCardProps {
containerWidth: number;
containerHeight: number;
Expand Down Expand Up @@ -194,7 +198,7 @@ function getSelectedLinksforStreamHover(singleLink: SLink): {
*/
// This is exported for unit tests.
export function groupNodesByColumn(graph: ISankeyChartData): NodesInColumns {
const nodesInColumn: NodesInColumns = {};
const nodesInColumn = createNodeIdMap<SNode[]>();
graph.nodes.forEach((node: SNode) => {
const columnId = node.layer!;
if (nodesInColumn[columnId]) {
Expand Down Expand Up @@ -331,20 +335,20 @@ function duplicateData(data: ISankeyChartData): ISankeyChartData {
}

function valuesOfNodes(nodes: SNode[]): NodeValues {
const result: NodeValues = {};
const result = createNodeIdMap<number>();
nodes.forEach((node: SNode) => {
result[node.nodeId as NodeId] = node.value!;
});
return result;
}

function valuesOfLinks(links: SLink[]): LinkValues {
const result: LinkValues = {};
const result = createNodeIdMap<ItemValues<number>>();
links.forEach((link: SLink) => {
const sourceId = idFromNumberOrSNode(link.source);
let sourceToTarget = result[sourceId];
if (!sourceToTarget) {
sourceToTarget = {};
sourceToTarget = createNodeIdMap<number>();
result[sourceId] = sourceToTarget;
}
sourceToTarget[idFromNumberOrSNode(link.target)] = link.value;
Expand Down Expand Up @@ -495,12 +499,12 @@ function computeLinkAttributes(
linkFrom: (node: SNode) => string,
linkAriaLabel: (link: SLink) => string,
): LinkItemValues<RenderedLinkAttributes> {
const result: LinkItemValues<RenderedLinkAttributes> = {};
const result = createNodeIdMap<ItemValues<RenderedLinkAttributes>>();
links.forEach((link: SLink) => {
const sourceId = idFromNumberOrSNode(link.source);
let sourceToTarget = result[sourceId];
if (!sourceToTarget) {
sourceToTarget = {};
sourceToTarget = createNodeIdMap<RenderedLinkAttributes>();
result[sourceId] = sourceToTarget;
}
sourceToTarget[idFromNumberOrSNode(link.target)] = {
Expand Down Expand Up @@ -928,7 +932,7 @@ export class SankeyChartBase extends React.Component<ISankeyChartProps, ISankeyC
nodes: SNode[],
nodeAriaLabel: (node: SNode, weight: number) => string,
): ItemValues<RenderedNodeAttributes> {
const result: ItemValues<RenderedNodeAttributes> = {};
const result = createNodeIdMap<RenderedNodeAttributes>();
const weightSpan = select('.nodeName').append('text').attr('class', 'tempText').append('tspan').text(null);
const nameSpan = select('.nodeName')
.append('text')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,17 @@ const dataWithoutColors: () => IChartProps = () => ({
},
});

const dataWithPrototypeNodeId: () => IChartProps = () => ({
chartTitle: 'Sankey Chart',
SankeyChartData: {
nodes: [
{ nodeId: '__proto__', name: 'Source' },
{ nodeId: 'target', name: 'Target' },
],
links: [{ source: 0, target: 1, value: 10 }],
},
});

describe('Sankey Chart snapShot testing', () => {
beforeEach(sharedBeforeEach);
afterEach(sharedAfterEach);
Expand Down Expand Up @@ -143,6 +154,13 @@ describe('Sankey Chart snapShot testing', () => {
expect(container).toMatchSnapshot();
});

it('renders Sankey correctly with a prototype property node ID', () => {
const { getByText } = render(<SankeyChart data={dataWithPrototypeNodeId()} height={500} width={800} />);

expect(getByText('Source')).toBeDefined();
expect(getByText('Target')).toBeDefined();
});

describe.skip('number formatting', () => {
it('renders Sankey correctly by formatting large numbers', () => {
// ARRANGE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,19 @@ function chartPointsWithStringNodeId(): ChartProps {
};
}

function chartPointsWithPrototypeNodeId(): ChartProps {
return {
chartTitle: 'Sankey Chart',
SankeyChartData: {
nodes: [
{ nodeId: '__proto__', name: 'Source' },
{ nodeId: 'target', name: 'Target' },
],
links: [{ source: 0, target: 1, value: 10 }],
},
};
}

const emptyChartPoints: ChartProps = {
chartData: [],
};
Expand Down Expand Up @@ -603,3 +616,17 @@ describe('SankeyChart - Min Height of Node Test', () => {
expect(component).toMatchSnapshot();
});
});

describe('Sankey chart prototype property node IDs', () => {
beforeEach(sharedBeforeEach);

testWithoutWait(
'Should render the Sankey chart with a prototype property node ID',
SankeyChart,
{ data: chartPointsWithPrototypeNodeId() },
() => {
expect(screen.getByText('Source')).toBeDefined();
expect(screen.getByText('Target')).toBeDefined();
},
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ type NodeValues = ItemValues<number>;
type LinkItemValues<T> = { [key: NodeId]: ItemValues<T> };
type LinkValues = LinkItemValues<number>;

type NodesInColumns = { [key: number]: SNode[] };
type NodesInColumns = ItemValues<SNode[]>;
type NormalizedData = SankeyChartData & {
width: number;
height: number;
Expand All @@ -37,6 +37,10 @@ type NormalizedData = SankeyChartData & {
type NodeColors = { fillColor: string; borderColor: string };
type SankeyLayoutGenerator = SankeyLayout<SankeyGraph<{}, {}>, {}, {}>;

function createNodeIdMap<T>(): ItemValues<T> {
return Object.setPrototypeOf({}, null);
}

const NON_SELECTED_NODE_AND_STREAM_COLOR: string = '#757575';
const DEFAULT_NODE_COLORS: NodeColors[] = [
{ fillColor: '#00758F', borderColor: '#002E39' },
Expand Down Expand Up @@ -162,7 +166,7 @@ function getSelectedLinksforStreamHover(singleLink: SLink): {
*/
// This is exported for unit tests.
export function groupNodesByColumn(graph: SankeyChartData): NodesInColumns {
const nodesInColumn: NodesInColumns = {};
const nodesInColumn = createNodeIdMap<SNode[]>();
graph.nodes.forEach((node: SNode) => {
const columnId = node.layer!;
if (nodesInColumn[columnId]) {
Expand Down Expand Up @@ -299,20 +303,20 @@ function duplicateData(data: SankeyChartData): SankeyChartData {
}

function valuesOfNodes(nodes: SNode[]): NodeValues {
const result: NodeValues = {};
const result = createNodeIdMap<number>();
nodes.forEach((node: SNode) => {
result[node.nodeId as NodeId] = node.value!;
});
return result;
}

function valuesOfLinks(links: SLink[]): LinkValues {
const result: LinkValues = {};
const result = createNodeIdMap<ItemValues<number>>();
links.forEach((link: SLink) => {
const sourceId = idFromNumberOrSNode(link.source);
let sourceToTarget = result[sourceId];
if (!sourceToTarget) {
sourceToTarget = {};
sourceToTarget = createNodeIdMap<number>();
result[sourceId] = sourceToTarget;
}
sourceToTarget[idFromNumberOrSNode(link.target)] = link.value;
Expand Down Expand Up @@ -464,12 +468,12 @@ function computeLinkAttributes(
linkAriaLabel: (link: SLink) => string,
linkId: string,
): LinkItemValues<RenderedLinkAttributes> {
const result: LinkItemValues<RenderedLinkAttributes> = {};
const result = createNodeIdMap<ItemValues<RenderedLinkAttributes>>();
links.forEach((link: SLink, index: number) => {
const sourceId = idFromNumberOrSNode(link.source);
let sourceToTarget = result[sourceId];
if (!sourceToTarget) {
sourceToTarget = {};
sourceToTarget = createNodeIdMap<RenderedLinkAttributes>();
result[sourceId] = sourceToTarget;
}
sourceToTarget[idFromNumberOrSNode(link.target)] = {
Expand Down Expand Up @@ -618,7 +622,7 @@ export const SankeyChart: React.FunctionComponent<SankeyChartProps> = React.forw

const _computeNodeAttributes = React.useCallback(
(nodes: SNode[], nodeAriaLabel: (node: SNode, weight: number) => string): ItemValues<RenderedNodeAttributes> => {
const result: ItemValues<RenderedNodeAttributes> = {};
const result = createNodeIdMap<RenderedNodeAttributes>();
const weightSpan = select('.nodeName').append('text').attr('class', 'tempText').append('tspan').text(null);
const nameSpan = select('.nodeName')
.append('text')
Expand Down
Loading