diff --git a/.github/workflows/canary_integration_tests.yml b/.github/workflows/canary_integration_tests.yml index 5c810062..804bf57e 100644 --- a/.github/workflows/canary_integration_tests.yml +++ b/.github/workflows/canary_integration_tests.yml @@ -38,6 +38,7 @@ jobs: "php/third_parties", "python/django", "python/lang", + "python/third_parties", "go/gorilla", "go/gosec", "go/lang", diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index 581c7b09..b54ea50e 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -44,6 +44,7 @@ jobs: "php/third_parties", "python/django", "python/lang", + "python/third_parties", "go/lang", "go/gosec", "go/gorilla", diff --git a/rules/python/shared/aws_lambda/user_input.yml b/rules/python/shared/aws_lambda/user_input.yml new file mode 100644 index 00000000..95b9517e --- /dev/null +++ b/rules/python/shared/aws_lambda/user_input.yml @@ -0,0 +1,10 @@ +type: shared +languages: + - python +patterns: + - | + def $<_>($event, context): + end +metadata: + description: "AWS Lambda user input." + id: python_shared_aws_lambda_user_input diff --git a/rules/python/shared/common/user_input.yml b/rules/python/shared/common/user_input.yml index 7c0b9a6d..f726790e 100644 --- a/rules/python/shared/common/user_input.yml +++ b/rules/python/shared/common/user_input.yml @@ -4,6 +4,7 @@ languages: imports: - python_shared_django_user_input - python_shared_lang_user_input + - python_shared_aws_lambda_user_input patterns: - pattern: $ filters: @@ -14,6 +15,9 @@ patterns: - variable: PYTHON_SHARED_COMMON_USER_INPUT detection: python_shared_lang_user_input scope: cursor_strict + - variable: PYTHON_SHARED_COMMON_USER_INPUT + detection: python_shared_aws_lambda_user_input + scope: cursor_strict metadata: description: "Python user input." id: python_shared_common_user_input diff --git a/rules/python/third_parties/aws_query_injection.yml b/rules/python/third_parties/aws_query_injection.yml new file mode 100644 index 00000000..ec6fa087 --- /dev/null +++ b/rules/python/third_parties/aws_query_injection.yml @@ -0,0 +1,149 @@ +imports: + - python_shared_common_user_input + - python_shared_lang_import1 +patterns: + # simple db + - pattern: $.select($<...>SelectExpression=$$<...>) + filters: + - variable: CALLER + detection: python_third_parties_aws_query_injection_simpledb_caller + scope: result + - variable: USER_INPUT + detection: python_third_parties_aws_query_injection_user_input + scope: result + # dynamodb + - pattern: $.$($<...>FilterExpression=$$<...>) + filters: + - variable: CALLER + detection: python_third_parties_aws_query_injection_dynamodb_caller + scope: result + - variable: METHOD + values: + - query + - scan + - variable: USER_INPUT + detection: python_third_parties_aws_query_injection_user_input + scope: result + - pattern: $.update_item($<...>UpdateExpression=$$<...>) + filters: + - variable: CALLER + detection: python_third_parties_aws_query_injection_dynamodb_caller + scope: result + - variable: USER_INPUT + detection: python_third_parties_aws_query_injection_user_input + scope: result + # legacy dynamodb + - pattern: $.query($<...>QueryFilter=$$<...>) + filters: + - variable: CALLER + detection: python_third_parties_aws_query_injection_dynamodb_caller + scope: result + - variable: USER_INPUT + detection: python_third_parties_aws_query_injection_user_input + scope: result + - pattern: $.scan($<...>ScanFilter=$$<...>) + filters: + - variable: CALLER + detection: python_third_parties_aws_query_injection_dynamodb_caller + scope: result + - variable: USER_INPUT + detection: python_third_parties_aws_query_injection_user_input + scope: result +auxiliary: + - id: python_third_parties_aws_query_injection_simpledb_caller + patterns: + - pattern: $($$<...>) + filters: + - variable: SIMPLEDB + string_regex: \Asdb\z + - variable: BOTO3_CLIENT + detection: python_shared_lang_import1 + scope: cursor + filters: + - variable: MODULE1 + values: [boto3] + - variable: NAME + values: [client] + - id: python_third_parties_aws_query_injection_dynamodb_caller + patterns: + - pattern: $.Table($<...>) + filters: + - variable: DYNAMODB_INIT + detection: python_third_parties_aws_query_injection_dynamodb_init + scope: result + - pattern: $($$<...>) + filters: + - variable: DYNAMODB + string_regex: \Adynamodb\z + - variable: BOTO3_CLIENT + detection: python_shared_lang_import1 + scope: cursor + filters: + - variable: MODULE1 + values: [boto3] + - variable: NAME + values: [client] + - id: python_third_parties_aws_query_injection_dynamodb_init + patterns: + - pattern: $($$<...>) + filters: + - variable: DYNAMODB + string_regex: \Adynamodb\z + - variable: BOTO3_RESOURCE + detection: python_shared_lang_import1 + scope: cursor + filters: + - variable: MODULE1 + values: [boto3] + - variable: NAME + values: [resource] + - id: python_third_parties_aws_query_injection_user_input + sanitizer: python_third_parties_aws_query_injection_sanitizer + patterns: + - pattern: $ + filters: + - variable: USER_INPUT + detection: python_shared_common_user_input + scope: cursor + - id: python_third_parties_aws_query_injection_sanitizer + patterns: + - pattern: | + { $<_>: $$<_> } +languages: + - python +severity: critical +metadata: + description: Unsanitized user input in AWS query + remediation_message: | + ## Description + + Including unsanitized data, such as user input or request data, in raw queries makes your application vulnerable to injection attacks. + + ## Remediations + + - **Do** always sanitize user input especially if it is to be used in database queries. Where possible, such sanitization should include the removal of special characters (like ' or ") that could be used to alter the semantics of a database query. + - **Do** validate user input wherever possible, to ensure it is the expected format and length + - **Do** use parameterized queries rather than concatenating user input directly into a query string. This separates query logic from user input, which is good practice. With DynamoDB, for example, you can make use of `ExpressionAttributeNames` and `ExpressionAttributeValues` parameters for this separation: + ```python + dynamodb = boto3.resource('dynamodb') + table = dynamodb.Table('users') + + table.update_item( + Key={ + 'username': 'johndoe', + 'last_name': 'Doe' + }, + UpdateExpression='SET age = :val1', + ExpressionAttributeValues={ + ':val1': 42 + } + ) + ``` + + ## References + - [AWS DynamoDB docs](https://docs.aws.amazon.com/dynamodb/) + - [AWS SimpleDB docs](https://docs.aws.amazon.com/AmazonSimpleDB/latest/DeveloperGuide/Welcome.html) + cwe_id: + - 943 + id: python_third_parties_aws_query_injection + documentation_url: https://docs.bearer.com/reference/rules/python_third_parties_aws_query_injection diff --git a/tests/python/third_parties/aws_query_injection/test.js b/tests/python/third_parties/aws_query_injection/test.js new file mode 100644 index 00000000..0f2d5e13 --- /dev/null +++ b/tests/python/third_parties/aws_query_injection/test.js @@ -0,0 +1,20 @@ +const { + createNewInvoker, + getEnvironment, +} = require("../../../helper.js") +const { ruleId, ruleFile, testBase } = getEnvironment(__dirname) + +describe(ruleId, () => { + const invoke = createNewInvoker(ruleId, ruleFile, testBase) + + test("aws_query_injection", () => { + const testCase = "main.py" + + const results = invoke(testCase) + + expect(results).toEqual({ + Missing: [], + Extra: [] + }) + }) +}) \ No newline at end of file diff --git a/tests/python/third_parties/aws_query_injection/testdata/main.py b/tests/python/third_parties/aws_query_injection/testdata/main.py new file mode 100644 index 00000000..21134e5e --- /dev/null +++ b/tests/python/third_parties/aws_query_injection/testdata/main.py @@ -0,0 +1,60 @@ +import boto3 + +DYNAMODB = 'dynamodb' +# dynamo db +def bad(event, context): + dynamodb = boto3.resource(DYNAMODB, '') + users_table = dynamodb.Table('users') + + # bearer:expected python_third_parties_aws_query_injection + users_table.query( + Select = 'ALL_ATTRIBUTES', + FilterExpression = event.body.filter + ) + + # bearer:expected python_third_parties_aws_query_injection + users_table.scan( + Select = 'ALL_ATTRIBUTES', + FilterExpression = event.body.filter + ) + + dynamodb = boto3.client('dynamodb') + # bearer:expected python_third_parties_aws_query_injection + dynamodb.query( + TableName='users', + FilterExpression = event.body.filter, + Select = "ALL_ATTRIBUTES" + ) + +# simple db +def bad2(event, context): + client = boto3.client('sdb') + # bearer:expected python_third_parties_aws_query_injection + client.select( + SelectExpression = event.body.select_expr, + NextToken = "string", + ConsistentRead = true + ) + +# dynamodb, simple db OK cases +def ok(event, context): + client = boto3.client('sdb') + client.select( + SelectExpression = "select * from 'users' where 'username' = 'john smith'", + NextToken = "string", + ConsistentRead = true + ) + + dynamodb = boto3.resource('dynamodb', '') + users_table = dynamodb.Table('users') + user_table.query( + Select = 'ALL_ATTRIBUTES', + FilterExpression = {'username': event.body.username}, + ) + + dynamodb = boto3.client('dynamodb') + dynamodb.query( + TableName='users', + FilterExpression = {'username': event.body.username}, + Select = "ALL_ATTRIBUTES" + )