Repository: serverless/serverless (JavaScript)
Date: 2016-11-17 12:08:32+00:00
Task type: Security

Issue:
Limit Lambda Permissions to created event source
<!--
1. Please check if an issue already exists so there are no duplicates
2. Check out and follow our Guidelines: https://github.com/serverless/serverless/blob/master/CONTRIBUTING.md
3. Fill out the whole template so we have a good overview on the issue
4. Do not remove any section of the template. If something is not applicable leave it empty but leave it in the Issue
5. Please follow the template, otherwise we'll have to ask you to update it
-->
# This is a Feature Proposal
## Description

When we create a LambdaPermission for sources to call a function (e.g. for S3 or SNS) we are not specific about which resource can call the function, but configure any resource of a specific service to be able to call the function (e.g. every S3 bucket or every SNS topic can call a LambdaFunction). This has to be limited so we only let specific resources call the lambda function by default.

This will not change behaviour in any way for the user, just tighten security.

Similar or dependent issues:
- #1895 
## Additional Data
- **_Serverless Framework Version you're using**_: latest master
- **_Operating System**_:
- **_Stack Trace**_:
- **_Provider Error messages**_:



Issue discussion:

Solution patch:
diff --git a/docs/providers/aws/guide/resources.md b/docs/providers/aws/guide/resources.md
index 2e2a7f67367..e5909d19b88 100644
--- a/docs/providers/aws/guide/resources.md
+++ b/docs/providers/aws/guide/resources.md
@@ -70,7 +70,7 @@ We're also using the term `normalizedName` or similar terms in this guide. This
 |IAM::Policy            | IamPolicyLambdaExecution                                | IamPolicyLambdaExecution      |
 |Lambda::Function       | {normalizedFunctionName}LambdaFunction                  | HelloLambdaFunction           |
 |Logs::LogGroup         | {normalizedFunctionName}LogGroup                        | HelloLogGroup                 |
-|Lambda::Permission     | <ul><li>**Schedule**: {normalizedFunctionName}LambdaPermissionEventsRuleSchedule{index} </li><li>**S3**: {normalizedFunctionName}LambdaPermissionS3</li><li>**APIG**: {normalizedFunctionName}LambdaPermissionApiGateway</li><li>**SNS**: {normalizedFunctionName}LambdaPermission{normalizedTopicName}</li> | <ul><li>**Schedule**: HelloLambdaPermissionEventsRuleSchedule1 </li><li>**S3**: HelloLambdaPermissionS3</li><li>**APIG**: HelloLambdaPermissionApiGateway</li><li>**SNS**: HelloLambdaPermissionSometopic</li> |
+|Lambda::Permission     | <ul><li>**Schedule**: {normalizedFunctionName}LambdaPermissionEventsRuleSchedule{index} </li><li>**S3**: {normalizedFunctionName}LambdaPermission{normalizedBucketName}S3</li><li>**APIG**: {normalizedFunctionName}LambdaPermissionApiGateway</li><li>**SNS**: {normalizedFunctionName}LambdaPermission{normalizedTopicName}SNS</li> | <ul><li>**Schedule**: HelloLambdaPermissionEventsRuleSchedule1 </li><li>**S3**: HelloLambdaPermissionBucketS3</li><li>**APIG**: HelloLambdaPermissionApiGateway</li><li>**SNS**: HelloLambdaPermissionTopicSNS</li> |
 |Events::Rule           | {normalizedFuntionName}EventsRuleSchedule{SequentialID} | HelloEventsRuleSchedule1      |
 |ApiGateway::RestApi    | ApiGatewayRestApi                                       | ApiGatewayRestApi             |
 |ApiGateway::Resource   | ApiGatewayResource{normalizedPath}                      | ApiGatewayResourceUsers       |
diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/method/index.js b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/method/index.js
index 07190ad869b..9353e14acc8 100644
--- a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/method/index.js
+++ b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/method/index.js
@@ -7,6 +7,7 @@ module.exports = {
 
   compileMethods() {
     this.apiGatewayMethodLogicalIds = [];
+    this.permissionMapping = [];
 
     this.validated.events.forEach((event) => {
       const resourceId = this.getResourceId(event.http.path);
@@ -27,15 +28,21 @@ module.exports = {
         template.Properties.ApiKeyRequired = true;
       }
 
+
+      const methodLogicalId = this.provider.naming
+        .getMethodLogicalId(resourceName, event.http.method);
+      const lambdaLogicalId = this.provider.naming
+        .getLambdaLogicalId(event.functionName);
+
+      const singlePermissionMapping = { resourceName, lambdaLogicalId, event };
+      this.permissionMapping.push(singlePermissionMapping);
+
       _.merge(template,
         this.getMethodAuthorization(event.http),
-        this.getMethodIntegration(event.http, event.functionName),
+        this.getMethodIntegration(event.http, lambdaLogicalId, methodLogicalId),
         this.getMethodResponses(event.http)
       );
 
-      const methodLogicalId = this.provider.naming
-        .getMethodLogicalId(resourceName, event.http.method);
-
       this.apiGatewayMethodLogicalIds.push(methodLogicalId);
 
       _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, {
diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/method/integration.js b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/method/integration.js
index 2d429b1807c..31645049c66 100644
--- a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/method/integration.js
+++ b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/method/integration.js
@@ -3,9 +3,7 @@
 const _ = require('lodash');
 
 module.exports = {
-  getMethodIntegration(http, functionName) {
-    const lambdaLogicalId = this.provider.naming
-      .getLambdaLogicalId(functionName);
+  getMethodIntegration(http, lambdaLogicalId) {
     const integration = {
       IntegrationHttpMethod: 'POST',
       Type: http.integration,
diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/permissions.js b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/permissions.js
index 3085327cfd0..3fe5d5546eb 100644
--- a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/permissions.js
+++ b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/permissions.js
@@ -6,27 +6,36 @@ const BbPromise = require('bluebird');
 module.exports = {
 
   compilePermissions() {
-    this.validated.events.forEach((event) => {
+    this.permissionMapping.forEach((singlePermissionMapping) => {
       const lambdaPermissionLogicalId = this.provider.naming
-        .getLambdaApiGatewayPermissionLogicalId(event.functionName);
-      const lambdaLogicalId = this.provider.naming
-        .getLambdaLogicalId(event.functionName);
+        .getLambdaApiGatewayPermissionLogicalId(singlePermissionMapping.event.functionName);
 
       _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, {
         [lambdaPermissionLogicalId]: {
           Type: 'AWS::Lambda::Permission',
           Properties: {
             FunctionName: {
-              'Fn::GetAtt': [lambdaLogicalId, 'Arn'],
+              'Fn::GetAtt': [singlePermissionMapping.lambdaLogicalId, 'Arn'],
             },
             Action: 'lambda:InvokeFunction',
             Principal: 'apigateway.amazonaws.com',
+            SourceArn: { 'Fn::Join': ['',
+              [
+                'arn:aws:execute-api:',
+                { Ref: 'AWS::Region' },
+                ':',
+                { Ref: 'AWS::AccountId' },
+                ':',
+                { Ref: this.apiGatewayRestApiLogicalId },
+                '/*/*',
+              ],
+            ] },
           },
         },
       });
 
-      if (event.http.authorizer) {
-        const authorizer = event.http.authorizer;
+      if (singlePermissionMapping.event.http.authorizer) {
+        const authorizer = singlePermissionMapping.event.http.authorizer;
         const authorizerPermissionLogicalId = this.provider.naming
           .getLambdaApiGatewayPermissionLogicalId(authorizer.name);
 
diff --git a/lib/plugins/aws/deploy/compile/events/s3/index.js b/lib/plugins/aws/deploy/compile/events/s3/index.js
index e98e883dfe5..0db67be13a3 100644
--- a/lib/plugins/aws/deploy/compile/events/s3/index.js
+++ b/lib/plugins/aws/deploy/compile/events/s3/index.js
@@ -119,7 +119,8 @@ class AwsCompileS3Events {
                 filter
               );
             }
-            s3EnabledFunctions.push(functionName);
+            const s3EnabledFunction = { functionName, bucketName };
+            s3EnabledFunctions.push(s3EnabledFunction);
           }
         });
       }
@@ -150,9 +151,9 @@ class AwsCompileS3Events {
     // iterate over all functions with S3 events
     // and give S3 permission to invoke them all
     // by adding Lambda::Permission resource for each
-    s3EnabledFunctions.forEach(functionName => {
+    s3EnabledFunctions.forEach(s3EnabledFunction => {
       const lambdaLogicalId = this.provider.naming
-        .getLambdaLogicalId(functionName);
+        .getLambdaLogicalId(s3EnabledFunction.functionName);
       const permissionTemplate = {
         Type: 'AWS::Lambda::Permission',
         Properties: {
@@ -164,10 +165,16 @@ class AwsCompileS3Events {
           },
           Action: 'lambda:InvokeFunction',
           Principal: 's3.amazonaws.com',
+          SourceArn: { 'Fn::Join': ['',
+            [
+              `arn:aws:s3:::${s3EnabledFunction.bucketName}`,
+            ],
+          ] },
         },
       };
       const lambdaPermissionLogicalId = this.provider.naming
-        .getLambdaS3PermissionLogicalId(functionName);
+        .getLambdaS3PermissionLogicalId(s3EnabledFunction.functionName,
+          s3EnabledFunction.bucketName);
       const permissionCFResource = {
         [lambdaPermissionLogicalId]: permissionTemplate,
       };
diff --git a/lib/plugins/aws/deploy/compile/events/sns/index.js b/lib/plugins/aws/deploy/compile/events/sns/index.js
index 83995d4c0af..abcc11c6656 100644
--- a/lib/plugins/aws/deploy/compile/events/sns/index.js
+++ b/lib/plugins/aws/deploy/compile/events/sns/index.js
@@ -79,7 +79,17 @@ class AwsCompileSNSEvents {
                 "Properties": {
                   "FunctionName": { "Fn::GetAtt": ["${lambdaLogicalId}", "Arn"] },
                   "Action": "lambda:InvokeFunction",
-                  "Principal": "sns.amazonaws.com"
+                  "Principal": "sns.amazonaws.com",
+                  "SourceArn": {
+                    "Fn::Join": ["",
+                      ["arn:aws:sns:",
+                      { "Ref": "AWS::Region"},
+                      ":",
+                      { "Ref": "AWS::AccountId"},
+                      ":",
+                      "${topicName}"]
+                     ]
+                  }
                 }
               }
             `;
diff --git a/lib/plugins/aws/lib/naming.js b/lib/plugins/aws/lib/naming.js
index a77d14a8afd..2fd5020ede6 100644
--- a/lib/plugins/aws/lib/naming.js
+++ b/lib/plugins/aws/lib/naming.js
@@ -136,12 +136,13 @@ module.exports = {
   },
 
   // Permissions
-  getLambdaS3PermissionLogicalId(functionName) {
-    return `${this.getNormalizedFunctionName(functionName)}LambdaPermissionS3`;
+  getLambdaS3PermissionLogicalId(functionName, bucketName) {
+    return `${this.getNormalizedFunctionName(functionName)}LambdaPermission${this
+      .normalizeBucketName(bucketName)}S3`;
   },
   getLambdaSnsPermissionLogicalId(functionName, topicName) {
     return `${this.getNormalizedFunctionName(functionName)}LambdaPermission${
-      this.normalizeTopicName(topicName)}`;
+      this.normalizeTopicName(topicName)}SNS`;
   },
   getLambdaSchedulePermissionLogicalId(functionName, scheduleIndex) {
     return `${this.getNormalizedFunctionName(functionName)}LambdaPermissionEventsRuleSchedule${


Test patch:
diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/tests/permissions.js b/lib/plugins/aws/deploy/compile/events/apiGateway/tests/permissions.js
index 6df28f26487..3602cab8f5f 100644
--- a/lib/plugins/aws/deploy/compile/events/apiGateway/tests/permissions.js
+++ b/lib/plugins/aws/deploy/compile/events/apiGateway/tests/permissions.js
@@ -18,7 +18,7 @@ describe('#awsCompilePermissions()', () => {
     awsCompileApigEvents.validated = {};
   });
 
-  it('should create permission resource when http events are given', () => {
+  it('should create limited permission resource scope to REST API', () => {
     awsCompileApigEvents.validated.events = [
       {
         functionName: 'First',
@@ -26,17 +26,19 @@ describe('#awsCompilePermissions()', () => {
           path: 'foo/bar',
           method: 'post',
         },
-      }, {
-        functionName: 'First',
-        http: {
-          path: 'foo/bar',
-          method: 'get',
-        },
-      }, {
-        functionName: 'Second',
-        http: {
-          path: 'bar/foo',
-          method: 'get',
+      },
+    ];
+    awsCompileApigEvents.apiGatewayRestApiLogicalId = 'ApiGatewayRestApi';
+    awsCompileApigEvents.permissionMapping = [
+      {
+        lambdaLogicalId: 'FirstLambdaFunction',
+        resourceName: 'FooBar',
+        event: {
+          http: {
+            path: 'foo/bar',
+            method: 'post',
+          },
+          functionName: 'First',
         },
       },
     ];
@@ -45,9 +47,22 @@ describe('#awsCompilePermissions()', () => {
       expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
         .Resources.FirstLambdaPermissionApiGateway
         .Properties.FunctionName['Fn::GetAtt'][0]).to.equal('FirstLambdaFunction');
+
+      const deepObj = { 'Fn::Join': ['',
+        [
+          'arn:aws:execute-api:',
+          { Ref: 'AWS::Region' },
+          ':',
+          { Ref: 'AWS::AccountId' },
+          ':',
+          { Ref: 'ApiGatewayRestApi' },
+          '/*/*',
+        ],
+      ] };
+
       expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
-        .Resources.SecondLambdaPermissionApiGateway
-        .Properties.FunctionName['Fn::GetAtt'][0]).to.equal('SecondLambdaFunction');
+        .Resources.FirstLambdaPermissionApiGateway
+        .Properties.SourceArn).to.deep.equal(deepObj);
     });
   });
 
@@ -65,6 +80,24 @@ describe('#awsCompilePermissions()', () => {
         },
       },
     ];
+    awsCompileApigEvents.apiGatewayRestApiLogicalId = 'ApiGatewayRestApi';
+    awsCompileApigEvents.permissionMapping = [
+      {
+        lambdaLogicalId: 'FirstLambdaFunction',
+        resourceName: 'FooBar',
+        event: {
+          http: {
+            authorizer: {
+              name: 'authorizer',
+              arn: { 'Fn::GetAtt': ['AuthorizerLambdaFunction', 'Arn'] },
+            },
+            path: 'foo/bar',
+            method: 'post',
+          },
+          functionName: 'First',
+        },
+      },
+    ];
     return awsCompileApigEvents.compilePermissions().then(() => {
       expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
         .Resources.AuthorizerLambdaPermissionApiGateway
@@ -74,6 +107,7 @@ describe('#awsCompilePermissions()', () => {
 
   it('should not create permission resources when http events are not given', () => {
     awsCompileApigEvents.validated.events = [];
+    awsCompileApigEvents.permissionMapping = [];
     return awsCompileApigEvents.compilePermissions().then(() => {
       expect(
         awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources
diff --git a/lib/plugins/aws/deploy/compile/events/s3/tests/index.js b/lib/plugins/aws/deploy/compile/events/s3/tests/index.js
index cf33a0a2999..529ca938a9c 100644
--- a/lib/plugins/aws/deploy/compile/events/s3/tests/index.js
+++ b/lib/plugins/aws/deploy/compile/events/s3/tests/index.js
@@ -118,7 +118,10 @@ describe('AwsCompileS3Events', () => {
         .Resources.S3BucketFirstfunctionbuckettwo.Type
       ).to.equal('AWS::S3::Bucket');
       expect(awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate
-        .Resources.FirstLambdaPermissionS3.Type
+        .Resources.FirstLambdaPermissionFirstfunctionbucketoneS3.Type
+      ).to.equal('AWS::Lambda::Permission');
+      expect(awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate
+        .Resources.FirstLambdaPermissionFirstfunctionbuckettwoS3.Type
       ).to.equal('AWS::Lambda::Permission');
       expect(awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate
         .Resources.S3BucketFirstfunctionbuckettwo.Properties.NotificationConfiguration
@@ -156,7 +159,7 @@ describe('AwsCompileS3Events', () => {
         .Resources.S3BucketFirstfunctionbucketone.Properties.NotificationConfiguration
         .LambdaConfigurations.length).to.equal(2);
       expect(awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate
-        .Resources.FirstLambdaPermissionS3.Type
+        .Resources.FirstLambdaPermissionFirstfunctionbucketoneS3.Type
       ).to.equal('AWS::Lambda::Permission');
     });
 
diff --git a/lib/plugins/aws/deploy/compile/events/sns/tests/index.js b/lib/plugins/aws/deploy/compile/events/sns/tests/index.js
index a0807480da7..e15006d01aa 100644
--- a/lib/plugins/aws/deploy/compile/events/sns/tests/index.js
+++ b/lib/plugins/aws/deploy/compile/events/sns/tests/index.js
@@ -62,10 +62,10 @@ describe('AwsCompileSNSEvents', () => {
         .provider.compiledCloudFormationTemplate.Resources.SNSTopicTopic2.Type
       ).to.equal('AWS::SNS::Topic');
       expect(awsCompileSNSEvents.serverless.service
-        .provider.compiledCloudFormationTemplate.Resources.FirstLambdaPermissionTopic1.Type
+        .provider.compiledCloudFormationTemplate.Resources.FirstLambdaPermissionTopic1SNS.Type
       ).to.equal('AWS::Lambda::Permission');
       expect(awsCompileSNSEvents.serverless.service
-        .provider.compiledCloudFormationTemplate.Resources.FirstLambdaPermissionTopic2.Type
+        .provider.compiledCloudFormationTemplate.Resources.FirstLambdaPermissionTopic2SNS.Type
       ).to.equal('AWS::Lambda::Permission');
     });
 
@@ -96,7 +96,7 @@ describe('AwsCompileSNSEvents', () => {
         .Properties.Subscription.length
       ).to.equal(2);
       expect(awsCompileSNSEvents.serverless.service
-        .provider.compiledCloudFormationTemplate.Resources.FirstLambdaPermissionTopic1.Type
+        .provider.compiledCloudFormationTemplate.Resources.FirstLambdaPermissionTopic1SNS.Type
       ).to.equal('AWS::Lambda::Permission');
     });
 
diff --git a/lib/plugins/aws/tests/naming.js b/lib/plugins/aws/tests/naming.js
index a2bd86fd1d3..7a106f9d1b7 100644
--- a/lib/plugins/aws/tests/naming.js
+++ b/lib/plugins/aws/tests/naming.js
@@ -325,16 +325,16 @@ describe('#naming()', () => {
 
   describe('#getLambdaS3PermissionLogicalId()', () => {
     it('should normalize the function name and add the standard suffix', () => {
-      expect(sdk.naming.getLambdaS3PermissionLogicalId('functionName'))
-        .to.equal('FunctionNameLambdaPermissionS3');
+      expect(sdk.naming.getLambdaS3PermissionLogicalId('functionName', 'bucket'))
+        .to.equal('FunctionNameLambdaPermissionBucketS3');
     });
   });
 
   describe('#getLambdaSnsPermissionLogicalId()', () => {
     it('should normalize the function and topic names and add them as prefix and suffix to the ' +
       'standard permission center', () => {
-      expect(sdk.naming.getLambdaSnsPermissionLogicalId('functionName', 'topicName'))
-        .to.equal('FunctionNameLambdaPermissionTopicName');
+      expect(sdk.naming.getLambdaSnsPermissionLogicalId('functionName', 'topic'))
+        .to.equal('FunctionNameLambdaPermissionTopicSNS');
     });
   });