Skip to content

Commit a93706d

Browse files
authored
Merge pull request #11227 from marmelab/fix-local-data-provider-prototype-pollution
Avoid prototype-polluting assignments in local data providers
2 parents 73b0971 + 6c2c632 commit a93706d

File tree

3 files changed

+198
-1
lines changed

3 files changed

+198
-1
lines changed

Agents.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ React-admin is a comprehensive frontend framework for building B2B and admin app
1919
- No children inspection — violates React patterns (exception: Datagrid)
2020
- No features achievable in pure React — keep the API surface small
2121
- No comments when code is self-explanatory
22+
- No dead code — trust your preconditions. Don't guard against conditions that prior code already prevents
23+
- DRY — don't duplicate knowledge. Coincidental code similarity is not duplication. Only deduplicate when the same decision or fact is expressed in multiple places. Code that looks alike but could evolve independently should stay separate
2224

2325
## Codebase Organization
2426

@@ -83,4 +85,4 @@ Every new feature or API change must be documented.
8385
make lint # ESLint checks
8486
make typecheck # TypeScript type checking
8587
make prettier # Prettier formatting
86-
```
88+
```
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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.each(['__proto__', 'constructor', 'prototype'])(
38+
'rejects unsafe resource key %s in update',
39+
async unsafeKey => {
40+
const dataProvider = localForageDataProvider();
41+
await expect(
42+
dataProvider.update(unsafeKey, {
43+
id: 1,
44+
data: { title: 'bad' },
45+
previousData: { id: 1 },
46+
} as any)
47+
).rejects.toThrow(`Invalid resource key: ${unsafeKey}`);
48+
}
49+
);
50+
51+
it.each(['__proto__', 'constructor', 'prototype'])(
52+
'rejects unsafe resource key %s in updateMany',
53+
async unsafeKey => {
54+
const dataProvider = localForageDataProvider();
55+
await expect(
56+
dataProvider.updateMany(unsafeKey, {
57+
ids: [1],
58+
data: { title: 'bad' },
59+
} as any)
60+
).rejects.toThrow(`Invalid resource key: ${unsafeKey}`);
61+
}
62+
);
63+
64+
it.each(['__proto__', 'constructor', 'prototype'])(
65+
'rejects unsafe resource key %s in create',
66+
async unsafeKey => {
67+
const dataProvider = localForageDataProvider();
68+
await expect(
69+
dataProvider.create(unsafeKey, {
70+
data: { title: 'bad' },
71+
} as any)
72+
).rejects.toThrow(`Invalid resource key: ${unsafeKey}`);
73+
}
74+
);
75+
76+
it.each(['__proto__', 'constructor', 'prototype'])(
77+
'rejects unsafe resource key %s in delete',
78+
async unsafeKey => {
79+
const dataProvider = localForageDataProvider();
80+
await expect(
81+
dataProvider.delete(unsafeKey, {
82+
id: 1,
83+
previousData: { id: 1 },
84+
} as any)
85+
).rejects.toThrow(`Invalid resource key: ${unsafeKey}`);
86+
}
87+
);
88+
89+
it.each(['__proto__', 'constructor', 'prototype'])(
90+
'rejects unsafe resource key %s in deleteMany',
91+
async unsafeKey => {
92+
const dataProvider = localForageDataProvider();
93+
await expect(
94+
dataProvider.deleteMany(unsafeKey, {
95+
ids: [1],
96+
} as any)
97+
).rejects.toThrow(`Invalid resource key: ${unsafeKey}`);
98+
}
99+
);
100+
});
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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.each(['__proto__', 'constructor', 'prototype'])(
33+
'rejects unsafe resource key %s in update',
34+
unsafeKey => {
35+
const dataProvider = localStorageDataProvider();
36+
expect(() =>
37+
dataProvider.update(unsafeKey, {
38+
id: 1,
39+
data: { title: 'bad' },
40+
previousData: { id: 1 },
41+
} as any)
42+
).toThrow(`Invalid resource key: ${unsafeKey}`);
43+
}
44+
);
45+
46+
it.each(['__proto__', 'constructor', 'prototype'])(
47+
'rejects unsafe resource key %s in updateMany',
48+
unsafeKey => {
49+
const dataProvider = localStorageDataProvider();
50+
expect(() =>
51+
dataProvider.updateMany(unsafeKey, {
52+
ids: [1],
53+
data: { title: 'bad' },
54+
} as any)
55+
).toThrow(`Invalid resource key: ${unsafeKey}`);
56+
}
57+
);
58+
59+
it.each(['__proto__', 'constructor', 'prototype'])(
60+
'rejects unsafe resource key %s in create',
61+
unsafeKey => {
62+
const dataProvider = localStorageDataProvider();
63+
expect(() =>
64+
dataProvider.create(unsafeKey, {
65+
data: { title: 'bad' },
66+
} as any)
67+
).toThrow(`Invalid resource key: ${unsafeKey}`);
68+
}
69+
);
70+
71+
it.each(['__proto__', 'constructor', 'prototype'])(
72+
'rejects unsafe resource key %s in delete',
73+
unsafeKey => {
74+
const dataProvider = localStorageDataProvider();
75+
expect(() =>
76+
dataProvider.delete(unsafeKey, {
77+
id: 1,
78+
previousData: { id: 1 },
79+
} as any)
80+
).toThrow(`Invalid resource key: ${unsafeKey}`);
81+
}
82+
);
83+
84+
it.each(['__proto__', 'constructor', 'prototype'])(
85+
'rejects unsafe resource key %s in deleteMany',
86+
unsafeKey => {
87+
const dataProvider = localStorageDataProvider();
88+
expect(() =>
89+
dataProvider.deleteMany(unsafeKey, {
90+
ids: [1],
91+
} as any)
92+
).toThrow(`Invalid resource key: ${unsafeKey}`);
93+
}
94+
);
95+
});

0 commit comments

Comments
 (0)