Skip to content

Commit 7f2f6f1

Browse files
authored
Merge pull request github#13729 from yoff/python/model-aws-lambdas
Python/JavaScript: Shared module for serverless functions
2 parents 66d13dc + b2988e5 commit 7f2f6f1

File tree

13 files changed

+455
-108
lines changed

13 files changed

+455
-108
lines changed
Lines changed: 40 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,120 +1,52 @@
11
/**
22
* Provides classes and predicates for working with serverless handlers.
33
* E.g. [AWS](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html) or [serverless](https://npmjs.com/package/serverless)
4-
*/
5-
6-
import javascript
7-
8-
/**
9-
* Provides classes and predicates for working with serverless handlers.
104
* In particular a `RemoteFlowSource` is added for AWS, Alibaba, and serverless.
115
*/
12-
private module ServerLess {
13-
/**
14-
* Holds if the `.yml` file `ymlFile` contains a serverless configuration with `handler` and `codeURI` properties.
15-
* `codeURI` defaults to the empty string if no explicit value is set in the configuration.
16-
*/
17-
private predicate hasServerlessHandler(File ymlFile, string handler, string codeUri) {
18-
exists(YamlMapping resource | ymlFile = resource.getFile() |
19-
// There exists at least "AWS::Serverless::Function" and "Aliyun::Serverless::Function"
20-
resource.lookup("Type").(YamlScalar).getValue().regexpMatch(".*::Serverless::Function") and
21-
exists(YamlMapping properties | properties = resource.lookup("Properties") |
22-
handler = properties.lookup("Handler").(YamlScalar).getValue() and
23-
if exists(properties.lookup("CodeUri"))
24-
then codeUri = properties.lookup("CodeUri").(YamlScalar).getValue()
25-
else codeUri = ""
26-
)
27-
or
28-
// The `serverless` library, which specifies a top-level `functions` property
29-
exists(YamlMapping functions |
30-
functions = resource.lookup("functions") and
31-
not exists(resource.getParentNode()) and
32-
handler = functions.getValue(_).(YamlMapping).lookup("handler").(YamlScalar).getValue() and
33-
codeUri = ""
34-
)
35-
)
36-
}
37-
38-
/**
39-
* Gets a string where an ending "/." is simplified to "/" (if it exists).
40-
*/
41-
bindingset[base]
42-
private string removeTrailingDot(string base) {
43-
if base.regexpMatch(".*/\\.")
44-
then result = base.substring(0, base.length() - 1)
45-
else result = base
46-
}
476

48-
/**
49-
* Gets a string where a leading "./" is simplified to "" (if it exists).
50-
*/
51-
bindingset[base]
52-
private string removeLeadingDotSlash(string base) {
53-
if base.regexpMatch("\\./.*") then result = base.substring(2, base.length()) else result = base
54-
}
7+
import javascript
8+
import codeql.serverless.ServerLess
559

56-
/**
57-
* Gets a path to a file from a `codeURI` property and a file name from a serverless configuration.
58-
*
59-
* For example if `codeURI` is "function/." and `file` is "index", then the result becomes "function/index.js".
60-
*/
61-
bindingset[codeUri, file]
62-
private string getPathFromHandlerProperties(string codeUri, string file) {
63-
exists(string folder | folder = removeLeadingDotSlash(removeTrailingDot(codeUri)) |
64-
result = folder + file + ".js"
65-
)
66-
}
10+
private module YamlImpl implements Input {
11+
import semmle.javascript.Files
12+
import semmle.javascript.YAML
13+
}
6714

68-
/**
69-
* Holds if `file` has a serverless handler function with name `func`.
70-
*/
71-
private predicate hasServerlessHandler(File file, string func) {
72-
exists(File ymlFile, string handler, string codeUri, string fileName |
73-
hasServerlessHandler(ymlFile, handler, codeUri) and
74-
// Splits a `handler` into two components. The `fileName` to the left of the dot, and the `func` to the right.
75-
// E.g. if `handler` is "index.foo", then `fileName` is "index" and `func` is "foo".
76-
exists(string pattern | pattern = "(.*)\\.(.*)" |
77-
fileName = handler.regexpCapture(pattern, 1) and
78-
func = handler.regexpCapture(pattern, 2)
79-
)
80-
|
81-
file.getAbsolutePath() =
82-
ymlFile.getParentContainer().getAbsolutePath() + "/" +
83-
getPathFromHandlerProperties(codeUri, fileName)
84-
)
85-
}
15+
module SL = ServerLess<YamlImpl>;
8616

87-
/**
88-
* Gets a function that is a serverless request handler.
89-
*
90-
* For example: if an AWS serverless resource contains the following properties (in the "template.yml" file):
91-
* ```yaml
92-
* Handler: mylibrary.handler
93-
* Runtime: nodejs12.x
94-
* CodeUri: backend/src/
95-
* ```
96-
*
97-
* And a file "mylibrary.js" exists in the folder "backend/src" (relative to the "template.yml" file).
98-
* Then the result of this predicate is a function exported as "handler" from "mylibrary.js".
99-
* The "mylibrary.js" file could for example look like:
100-
*
101-
* ```JavaScript
102-
* module.exports.handler = function (event) { ... }
103-
* ```
104-
*/
105-
private DataFlow::FunctionNode getAServerlessHandler() {
106-
exists(File file, string handler, Module mod | hasServerlessHandler(file, handler) |
107-
mod.getFile() = file and
108-
result = mod.getAnExportedValue(handler).getAFunctionValue()
109-
)
110-
}
17+
/**
18+
* Gets a function that is a serverless request handler.
19+
*
20+
* For example: if an AWS serverless resource contains the following properties (in the "template.yml" file):
21+
* ```yaml
22+
* Handler: mylibrary.handler
23+
* Runtime: nodejs12.x
24+
* CodeUri: backend/src/
25+
* ```
26+
*
27+
* And a file "mylibrary.js" exists in the folder "backend/src" (relative to the "template.yml" file).
28+
* Then the result of this predicate is a function exported as "handler" from "mylibrary.js".
29+
* The "mylibrary.js" file could for example look like:
30+
*
31+
* ```JavaScript
32+
* module.exports.handler = function (event) { ... }
33+
* ```
34+
*/
35+
private DataFlow::FunctionNode getAServerlessHandler() {
36+
exists(File file, string stem, string handler, Module mod |
37+
SL::hasServerlessHandler(stem, handler, _, _) and
38+
file.getAbsolutePath() = stem + ".js"
39+
|
40+
mod.getFile() = file and
41+
result = mod.getAnExportedValue(handler).getAFunctionValue()
42+
)
43+
}
11144

112-
/**
113-
* A serverless request handler event, seen as a RemoteFlowSource.
114-
*/
115-
private class ServerlessHandlerEventAsRemoteFlow extends RemoteFlowSource {
116-
ServerlessHandlerEventAsRemoteFlow() { this = getAServerlessHandler().getParameter(0) }
45+
/**
46+
* A serverless request handler event, seen as a RemoteFlowSource.
47+
*/
48+
private class ServerlessHandlerEventAsRemoteFlow extends RemoteFlowSource {
49+
ServerlessHandlerEventAsRemoteFlow() { this = getAServerlessHandler().getParameter(0) }
11750

118-
override string getSourceType() { result = "Serverless event" }
119-
}
51+
override string getSourceType() { result = "Serverless event" }
12052
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* Added modeling of AWS Lambda handlers that can be identified with `AWS::Serverless::Function` in YAML files, where the event parameter is modeled as a remote-flow-source.

python/ql/lib/semmle/python/Frameworks.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ private import semmle.python.frameworks.Requests
4949
private import semmle.python.frameworks.RestFramework
5050
private import semmle.python.frameworks.Rsa
5151
private import semmle.python.frameworks.RuamelYaml
52+
private import semmle.python.frameworks.ServerLess
5253
private import semmle.python.frameworks.Simplejson
5354
private import semmle.python.frameworks.SqlAlchemy
5455
private import semmle.python.frameworks.Starlette
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* Provides classes and predicates for working with those serverless handlers,
3+
* handled by the shared library.
4+
*
5+
* E.g. [AWS](https://docs.aws.amazon.com/lambda/latest/dg/python-handler.html).
6+
*
7+
* In particular a `RemoteFlowSource` is added for each.
8+
*/
9+
10+
import python
11+
import codeql.serverless.ServerLess
12+
import semmle.python.dataflow.new.DataFlow
13+
import semmle.python.dataflow.new.RemoteFlowSources
14+
15+
private module YamlImpl implements Input {
16+
import semmle.python.Files
17+
import semmle.python.Yaml
18+
}
19+
20+
module SL = ServerLess<YamlImpl>;
21+
22+
/**
23+
* Gets a function that is a serverless request handler.
24+
*
25+
* For example: if an AWS serverless resource contains the following properties (in the "template.yml" file):
26+
* ```yaml
27+
* Handler: mylibrary.handler
28+
* Runtime: pythonXXX
29+
* CodeUri: backend/src/
30+
* ```
31+
*
32+
* And a file "mylibrary.py" exists in the folder "backend/src" (relative to the "template.yml" file).
33+
* Then the result of this predicate is a function exported as "handler" from "mylibrary.py".
34+
* The "mylibrary.py" file could for example look like:
35+
*
36+
* ```python
37+
* def handler(event):
38+
* ...
39+
* ```
40+
*/
41+
private Function getAServerlessHandler() {
42+
exists(File file, string stem, string handler, string runtime, Module mod |
43+
SL::hasServerlessHandler(stem, handler, _, runtime) and
44+
file.getAbsolutePath() = stem + ".py" and
45+
// if runtime is specified, it should be python
46+
(runtime = "" or runtime.matches("python%"))
47+
|
48+
mod.getFile() = file and
49+
result.getScope() = mod and
50+
result.getName() = handler
51+
)
52+
}
53+
54+
private DataFlow::ParameterNode getAHandlerEventParameter() {
55+
exists(Function func | func = getAServerlessHandler() |
56+
result.getParameter() in [func.getArg(0), func.getArgByName("event")]
57+
)
58+
}
59+
60+
/**
61+
* A serverless request handler event, seen as a RemoteFlowSource.
62+
*/
63+
private class ServerlessHandlerEventAsRemoteFlow extends RemoteFlowSource::Range {
64+
ServerlessHandlerEventAsRemoteFlow() { this = getAHandlerEventParameter() }
65+
66+
override string getSourceType() { result = "Serverless event" }
67+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
failures
2+
argumentToEnsureNotTaintedNotMarkedAsSpurious
3+
untaintedArgumentToEnsureTaintedNotMarkedAsMissing
4+
testFailures
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import experimental.meta.InlineTaintTest
2+
import MakeInlineTaintTest<TestTaintTrackingConfig>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
def handler1(event, context):
2+
ensure_tainted(event) # $ tainted
3+
return "Hello World!"
4+
5+
def handler2(event, context):
6+
ensure_tainted(event) # $ tainted
7+
return "Hello World!"
8+
9+
# This function is not mentioned in template.yml
10+
# and so it is not receiving user input.
11+
def non_handler(event, context):
12+
ensure_not_tainted(event)
13+
return "Hello World!"
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
def lambda_handler(event, context):
2+
ensure_tainted(
3+
event, # $ tainted
4+
# event is usually a dict, see https://docs.aws.amazon.com/lambda/latest/dg/python-handler.html
5+
event["key"], # $ tainted
6+
event["key"]["key2"], # $ tainted
7+
event["key"][0], # $ tainted
8+
# but can also be a list
9+
event[0], # $ tainted
10+
)
11+
return "OK"
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# inspired by https://github.com/awsdocs/aws-lambda-developer-guide/blob/main/sample-apps/blank-python/template.yml
2+
# but we have added extra handlers
3+
AWSTemplateFormatVersion: '2010-09-09'
4+
Transform: 'AWS::Serverless-2016-10-31'
5+
Description: An AWS Lambda application that calls the Lambda API.
6+
Resources:
7+
function:
8+
Type: AWS::Serverless::Function
9+
Properties:
10+
Handler: lambda_function.lambda_handler
11+
Runtime: python3.8
12+
CodeUri: function/.
13+
Description: Call the AWS Lambda API
14+
Timeout: 10
15+
# Function's execution role
16+
Policies:
17+
- AWSLambdaBasicExecutionRole
18+
- AWSLambda_ReadOnlyAccess
19+
- AWSXrayWriteOnlyAccess
20+
Tracing: Active
21+
Layers:
22+
- !Ref libs
23+
function:
24+
Type: AWS::Serverless::Function
25+
Properties:
26+
Handler: extra_lambdas.handler1
27+
Runtime: python3.8
28+
CodeUri: function/.
29+
Description: Call the AWS Lambda API
30+
Timeout: 10
31+
# Function's execution role
32+
Policies:
33+
- AWSLambdaBasicExecutionRole
34+
- AWSLambda_ReadOnlyAccess
35+
- AWSXrayWriteOnlyAccess
36+
Tracing: Active
37+
Layers:
38+
- !Ref libs
39+
function:
40+
Type: AWS::Serverless::Function
41+
Properties:
42+
Handler: extra_lambdas.handler2
43+
Runtime: python3.8
44+
CodeUri: function/.
45+
Description: Call the AWS Lambda API
46+
Timeout: 10
47+
# Function's execution role
48+
Policies:
49+
- AWSLambdaBasicExecutionRole
50+
- AWSLambda_ReadOnlyAccess
51+
- AWSXrayWriteOnlyAccess
52+
Tracing: Active
53+
Layers:
54+
- !Ref libs
55+
libs:
56+
Type: AWS::Serverless::LayerVersion
57+
Properties:
58+
LayerName: blank-python-lib
59+
Description: Dependencies for the blank-python sample app.
60+
ContentUri: package/.
61+
CompatibleRuntimes:
62+
- python3.8
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
semmle-extractor-options: -R .

0 commit comments

Comments
 (0)