Skip to content

Commit 55952eb

Browse files
committed
Tests: Add security tests
1 parent 05edbfc commit 55952eb

3 files changed

Lines changed: 59 additions & 7 deletions

File tree

README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ Declarative Mapper is a JSON data transformation and object mapping library for
1616
- [Overview](#overview)
1717
- [Reasoning](#reasoning)
1818
- [Quick Start Example](#quick-start-example)
19-
- [Compatibility](#compatibility)
19+
- [Compatibility](#compatibility)
20+
- [Security](#security)
2021
- [Mapping Instructions](#mapping-instructions)
2122
- [Runtime Variables Quick Reference](#runtime-variables-quick-reference)
2223
- [Objects](#objects)
@@ -81,11 +82,18 @@ const mapper = createMapper({
8182
const results = sourceOrders.map(mapper);
8283
```
8384

84-
### Compatibility
85+
## Compatibility
8586

8687
- **Node.js:** 16+
8788
- **Browser:** best effort support; requires a `vm` polyfill
8889

90+
## Security
91+
92+
Similar mappings can be achieved with plain JavaScript, but this library is designed for a different case: user-controlled mapping templates executed on the server.
93+
94+
Mappings stay simple for non-technical users, while technical users can still use JavaScript expressions.
95+
Instead of `eval`, expressions run in an isolated VM context with built-ins, mapping input, and explicit `extensions` only, which reduces JS injection risk.
96+
8997
## Mapping Instructions
9098

9199
In mapping JSON, the left side is a key in the resulting object.

src/createMapper.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,21 +52,21 @@ export default function createMapper<TSource extends object, TResult>(map: RootM
5252
...options?.extensions
5353
};
5454

55-
const context = vm.createContext(sandbox);
55+
const ctx = vm.createContext(sandbox);
5656

5757
return (document: TSource): TResult | undefined => {
5858

59-
context.$input = document;
60-
context.$result = undefined;
59+
ctx.$input = document;
60+
ctx.$result = undefined;
6161

6262
if (extensionNames) {
6363
const conflictingKey = Object.keys(document).find(inputKey => extensionNames.includes(inputKey));
6464
if (conflictingKey)
6565
throw new TypeError(`Extension "${conflictingKey}" conflicts with a field name passed in input`);
6666
}
6767

68-
script.runInContext(context);
68+
script.runInContext(ctx);
6969

70-
return context.$result;
70+
return ctx.$result;
7171
};
7272
}

tests/unit/createMapper.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,50 @@ with ($createGlobalContext($input)) {
424424
});
425425
});
426426

427+
describe('security', () => {
428+
429+
it('does not expose process/global objects to mapping expressions', () => {
430+
431+
const mapper = createMapper({
432+
directProcess: 'process',
433+
globalThisProcess: 'globalThis?.process'
434+
});
435+
436+
const result = mapper({});
437+
438+
expect(result).to.eql({
439+
directProcess: undefined,
440+
globalThisProcess: undefined
441+
});
442+
});
443+
444+
it('blocks constructor-based access to process', () => {
445+
446+
const mapper = createMapper({
447+
value: '(() => { try { return [].filter.constructor("return process")().pid; } catch (e) { return "blocked"; } })()'
448+
});
449+
450+
const result = mapper({});
451+
452+
expect(result).to.eql({
453+
value: 'blocked'
454+
});
455+
});
456+
457+
it('blocks Function-based require access', () => {
458+
459+
const mapper = createMapper({
460+
value: '(() => { try { return Function("return require(\\"fs\\")")(); } catch (e) { return "blocked"; } })()'
461+
});
462+
463+
const result = mapper({});
464+
465+
expect(result).to.eql({
466+
value: 'blocked'
467+
});
468+
});
469+
});
470+
427471
describe('*', () => {
428472

429473
it('maps result from simple type', () => {

0 commit comments

Comments
 (0)