Skip to content

Commit 28d8cd8

Browse files
committed
Avoid prototype-polluting assignments in local data providers
1 parent 9676993 commit 28d8cd8

File tree

4 files changed

+188
-34
lines changed

4 files changed

+188
-34
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import expect from 'expect';
2+
import localforage from 'localforage';
3+
4+
import localForageDataProvider from './index';
5+
6+
jest.mock('localforage', () => ({
7+
__esModule: true,
8+
default: {
9+
keys: jest.fn(),
10+
getItem: jest.fn(),
11+
setItem: jest.fn(),
12+
},
13+
}));
14+
15+
describe('ra-data-local-forage', () => {
16+
beforeEach(() => {
17+
jest.resetAllMocks();
18+
(localforage.keys as jest.Mock).mockResolvedValue([]);
19+
(localforage.getItem as jest.Mock).mockResolvedValue(undefined);
20+
(localforage.setItem as jest.Mock).mockResolvedValue(undefined);
21+
});
22+
23+
it('creates missing resource collections safely', async () => {
24+
const dataProvider = localForageDataProvider();
25+
26+
const response = await dataProvider.create('posts', {
27+
data: { title: 'Hello world' },
28+
} as any);
29+
30+
expect(response.data.title).toEqual('Hello world');
31+
expect(localforage.setItem).toHaveBeenCalledWith(
32+
'ra-data-local-forage-posts',
33+
[expect.objectContaining({ title: 'Hello world' })]
34+
);
35+
});
36+
37+
it('rejects unsafe resource keys', async () => {
38+
const dataProvider = localForageDataProvider();
39+
40+
await expect(
41+
dataProvider.update('__proto__', {
42+
id: 1,
43+
data: { title: 'bad' },
44+
previousData: { id: 1 },
45+
} as any)
46+
).rejects.toThrow('Invalid resource key: __proto__');
47+
});
48+
});

packages/ra-data-local-forage/src/index.ts

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -175,11 +175,12 @@ export default (params?: LocalForageDataProviderParams): DataProvider => {
175175
throw new Error('The dataProvider is not initialized.');
176176
}
177177

178-
const index = data[resource].findIndex(
178+
const resourceData = getResourceCollection(data, resource);
179+
const index = resourceData.findIndex(
179180
(record: { id: any }) => record.id === params.id
180181
);
181-
data[resource][index] = {
182-
...data[resource][index],
182+
resourceData[index] = {
183+
...resourceData[index],
183184
...params.data,
184185
};
185186
updateLocalForage(resource);
@@ -191,16 +192,18 @@ export default (params?: LocalForageDataProviderParams): DataProvider => {
191192
if (!baseDataProvider) {
192193
throw new Error('The dataProvider is not initialized.');
193194
}
195+
if (!data) {
196+
throw new Error('The dataProvider is not initialized.');
197+
}
198+
199+
const resourceData = getResourceCollection(data, resource);
194200

195201
params.ids.forEach((id: Identifier) => {
196-
if (!data) {
197-
throw new Error('The dataProvider is not initialized.');
198-
}
199-
const index = data[resource].findIndex(
202+
const index = resourceData.findIndex(
200203
(record: { id: Identifier }) => record.id === id
201204
);
202-
data[resource][index] = {
203-
...data[resource][index],
205+
resourceData[index] = {
206+
...resourceData[index],
204207
...params.data,
205208
};
206209
});
@@ -223,10 +226,11 @@ export default (params?: LocalForageDataProviderParams): DataProvider => {
223226
if (!data) {
224227
throw new Error('The dataProvider is not initialized.');
225228
}
226-
if (!data.hasOwnProperty(resource)) {
227-
data[resource] = [];
228-
}
229-
data[resource].push(response.data);
229+
const resourceData = getOrCreateResourceCollection(
230+
data,
231+
resource
232+
);
233+
resourceData.push(response.data);
230234
updateLocalForage(resource);
231235
return response;
232236
});
@@ -243,10 +247,11 @@ export default (params?: LocalForageDataProviderParams): DataProvider => {
243247
if (!data) {
244248
throw new Error('The dataProvider is not initialized.');
245249
}
246-
const index = data[resource].findIndex(
250+
const resourceData = getResourceCollection(data, resource);
251+
const index = resourceData.findIndex(
247252
(record: { id: any }) => record.id === params.id
248253
);
249-
pullAt(data[resource], [index]);
254+
pullAt(resourceData, [index]);
250255
updateLocalForage(resource);
251256
return baseDataProvider.delete<RecordType>(resource, params);
252257
},
@@ -259,21 +264,48 @@ export default (params?: LocalForageDataProviderParams): DataProvider => {
259264
if (!data) {
260265
throw new Error('The dataProvider is not initialized.');
261266
}
267+
const resourceData = getResourceCollection(data, resource);
262268
const indexes = params.ids.map((id: any) => {
263-
if (!data) {
264-
throw new Error('The dataProvider is not initialized.');
265-
}
266-
return data[resource].findIndex(
269+
return resourceData.findIndex(
267270
(record: any) => record.id === id
268271
);
269272
});
270-
pullAt(data[resource], indexes);
273+
pullAt(resourceData, indexes);
271274
updateLocalForage(resource);
272275
return baseDataProvider.deleteMany(resource, params);
273276
},
274277
};
275278
};
276279

280+
const getResourceCollection = (data: Record<string, any>, resource: string) => {
281+
const resourceData = data[resource];
282+
283+
if (!resourceData) {
284+
throw new Error(`Unknown resource key: ${resource}`);
285+
}
286+
287+
return resourceData;
288+
};
289+
290+
const getOrCreateResourceCollection = (
291+
data: Record<string, any>,
292+
resource: string
293+
) => {
294+
const resourceData = data[resource];
295+
if (resourceData) {
296+
return resourceData;
297+
}
298+
299+
Object.defineProperty(data, resource, {
300+
value: [],
301+
writable: true,
302+
enumerable: true,
303+
configurable: true,
304+
});
305+
306+
return data[resource];
307+
};
308+
277309
const checkResource = resource => {
278310
if (['__proto__', 'constructor', 'prototype'].includes(resource)) {
279311
// protection against prototype pollution
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import expect from 'expect';
2+
3+
import localStorageDataProvider from './index';
4+
5+
describe('ra-data-local-storage', () => {
6+
beforeEach(() => {
7+
localStorage.clear();
8+
});
9+
10+
it('creates missing resource collections safely', async () => {
11+
const dataProvider = localStorageDataProvider({
12+
localStorageKey: 'ra-data-local-storage-test',
13+
localStorageUpdateDelay: 0,
14+
});
15+
16+
const response = await dataProvider.create('posts', {
17+
data: { title: 'Hello world' },
18+
} as any);
19+
20+
await new Promise(resolve => setTimeout(resolve, 0));
21+
22+
expect(response.data.title).toEqual('Hello world');
23+
expect(
24+
JSON.parse(
25+
localStorage.getItem('ra-data-local-storage-test') || '{}'
26+
)
27+
).toMatchObject({
28+
posts: [expect.objectContaining({ title: 'Hello world' })],
29+
});
30+
});
31+
32+
it('rejects unsafe resource keys', () => {
33+
const dataProvider = localStorageDataProvider();
34+
35+
expect(() =>
36+
dataProvider.update('__proto__', {
37+
id: 1,
38+
data: { title: 'bad' },
39+
previousData: { id: 1 },
40+
} as any)
41+
).toThrow('Invalid resource key: __proto__');
42+
});
43+
});

packages/ra-data-local-storage/src/index.ts

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,12 @@ export default (params?: LocalStorageDataProviderParams): DataProvider => {
100100
update: <RecordType extends RaRecord = any>(resource, params) => {
101101
checkResource(resource);
102102
updateLocalStorage(() => {
103-
const index = data[resource]?.findIndex(
103+
const resourceData = getResourceCollection(data, resource);
104+
const index = resourceData.findIndex(
104105
record => record.id == params.id
105106
);
106-
data[resource][index] = {
107-
...data[resource][index],
107+
resourceData[index] = {
108+
...resourceData[index],
108109
...params.data,
109110
};
110111
});
@@ -113,12 +114,13 @@ export default (params?: LocalStorageDataProviderParams): DataProvider => {
113114
updateMany: (resource, params) => {
114115
checkResource(resource);
115116
updateLocalStorage(() => {
117+
const resourceData = getResourceCollection(data, resource);
116118
params.ids.forEach(id => {
117-
const index = data[resource]?.findIndex(
119+
const index = resourceData.findIndex(
118120
record => record.id == id
119121
);
120-
data[resource][index] = {
121-
...data[resource][index],
122+
resourceData[index] = {
123+
...resourceData[index],
122124
...params.data,
123125
};
124126
});
@@ -135,37 +137,66 @@ export default (params?: LocalStorageDataProviderParams): DataProvider => {
135137
.create<RecordType>(resource, params)
136138
.then(response => {
137139
updateLocalStorage(() => {
138-
if (!data.hasOwnProperty(resource)) {
139-
data[resource] = [];
140-
}
141-
data[resource].push(response.data);
140+
const resourceData = getOrCreateResourceCollection(
141+
data,
142+
resource
143+
);
144+
resourceData.push(response.data);
142145
});
143146
return response;
144147
});
145148
},
146149
delete: <RecordType extends RaRecord = any>(resource, params) => {
147150
checkResource(resource);
148151
updateLocalStorage(() => {
149-
const index = data[resource]?.findIndex(
152+
const resourceData = getResourceCollection(data, resource);
153+
const index = resourceData.findIndex(
150154
record => record.id == params.id
151155
);
152-
pullAt(data[resource], [index]);
156+
pullAt(resourceData, [index]);
153157
});
154158
return baseDataProvider.delete<RecordType>(resource, params);
155159
},
156160
deleteMany: (resource, params) => {
157161
checkResource(resource);
158162
updateLocalStorage(() => {
163+
const resourceData = getResourceCollection(data, resource);
159164
const indexes = params.ids.map(id =>
160-
data[resource]?.findIndex(record => record.id == id)
165+
resourceData.findIndex(record => record.id == id)
161166
);
162-
pullAt(data[resource], indexes);
167+
pullAt(resourceData, indexes);
163168
});
164169
return baseDataProvider.deleteMany(resource, params);
165170
},
166171
};
167172
};
168173

174+
const getResourceCollection = (data, resource) => {
175+
const resourceData = data[resource];
176+
177+
if (!resourceData) {
178+
throw new Error(`Unknown resource key: ${resource}`);
179+
}
180+
181+
return resourceData;
182+
};
183+
184+
const getOrCreateResourceCollection = (data, resource) => {
185+
const resourceData = data[resource];
186+
if (resourceData) {
187+
return resourceData;
188+
}
189+
190+
Object.defineProperty(data, resource, {
191+
value: [],
192+
writable: true,
193+
enumerable: true,
194+
configurable: true,
195+
});
196+
197+
return data[resource];
198+
};
199+
169200
const checkResource = resource => {
170201
if (['__proto__', 'constructor', 'prototype'].includes(resource)) {
171202
// protection against prototype pollution

0 commit comments

Comments
 (0)