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
2 changes: 2 additions & 0 deletions docs/guides/variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,8 @@ functions:
- schedule: ${file(./scheduleConfig.js):rate} # Reference a specific module
```

Address resolution follows the value's own properties. If a JS file exports (or its resolver function returns) a `Proxy`-backed object, address segments are resolved through the proxy's `get` handler instead. The keys `__proto__`, `prototype` and `constructor` are only followed when they are own properties of the value, so an address can never traverse into prototype internals.

### Exporting a function

_Note: the method described below works by default in Serverless v3, but it requires the `variablesResolutionMode: 20210326` option in v2. In v3, `variablesResolutionMode` is obsolete; the old `20210219` mode is scheduled to be rejected in osls v4.0.0, and the property itself is scheduled for removal in osls v5.0.0._
Expand Down
10 changes: 9 additions & 1 deletion lib/configuration/variables/sources/file.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ const ensureString = require('type/string/ensure');
const isPlainFunction = require('type/plain-function/is');
const path = require('path');
const fsp = require('fs').promises;
const { isProxy } = require('util').types;
const yaml = require('js-yaml');
const cloudformationSchema = require('../../../utils/serverless-utils/cloudformation-schema');
const ServerlessError = require('../../../serverless-error');
const { hasOwn } = require('../../../utils/safe-object');
const { isUnsafePropertyKey } = require('../../../utils/object-path');

const readFile = async (filePath, serviceDir) => {
try {
Expand Down Expand Up @@ -150,7 +152,13 @@ module.exports = {
let result = content;
for (const propertyKey of propertyKeys) {
if (typeof result === 'string') result = await resolveVariablesInString(result);
if (result == null || !hasOwn(result, propertyKey)) return { value: null };
if (result == null) return { value: null };
if (!hasOwn(result, propertyKey)) {
// `hasOwnProperty` does not consult Proxy "has"/"get" traps, so virtual
// properties of Proxy-backed JS file results must be resolved through
// property access. Prototype internals stay blocked unless claimed as own.
if (!isProxy(result) || isUnsafePropertyKey(propertyKey)) return { value: null };
}
result = result[propertyKey];
if (result == null) return { value: null };
if (!isResolvedByFunction) {
Expand Down
14 changes: 14 additions & 0 deletions test/unit/lib/configuration/variables/sources/file.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ describe('test/unit/lib/configuration/variables/sources/file.test.js', () => {
arrayLengthAddress: '${file(file-array.json):items.length}',
unsafeOwnProtoAddress: '${file(file-unsafe-keys.json):__proto__.value}',
inheritedConstructorAddress: '${file(file-unsafe-keys.json):constructor.name, null}',
proxyVirtualAddress: '${file(file-proxy.js):proxy.virtual.nested}',
proxyOwnAddress: '${file(file-proxy.js):proxy.own}',
proxyMissingAddress: '${file(file-proxy.js):proxy.missing, null}',
proxyInheritedConstructorAddress: '${file(file-proxy.js):proxy.constructor.name, null}',
nonExistingYaml: '${file(not-existing.yaml), null}',
nonExistingJson: '${file(not-existing.json), null}',
nonExistingJs: '${file(not-existing.js), null}',
Expand Down Expand Up @@ -130,6 +134,16 @@ describe('test/unit/lib/configuration/variables/sources/file.test.js', () => {
expect(configuration.inheritedConstructorAddress).to.equal(null);
});

it('should resolve Proxy-backed virtual properties across file address resolution', () => {
expect(configuration.proxyVirtualAddress).to.equal('proxy-virtual');
expect(configuration.proxyOwnAddress).to.equal('own-value');
expect(configuration.proxyMissingAddress).to.equal(null);
});

it('should not traverse unsafe keys on Proxy-backed file results', () => {
expect(configuration.proxyInheritedConstructorAddress).to.equal(null);
});

it('should unconditionally split "address" property keys by "."', () =>
expect(configuration.ambiguousAddress).to.equal('object'));

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use strict';

const virtualProperties = { virtual: { nested: 'proxy-virtual' } };

module.exports = {
proxy: new Proxy(
{ own: 'own-value' },
{
get(target, key) {
if (key in virtualProperties) return virtualProperties[key];
return target[key];
},
}
),
};
Loading