Skip to content

Commit b8fde51

Browse files
authored
chore: restructure and rename python checkers (#60)
* chore: restructure checkers to follow arbitrary checker directory Signed-off-by: Maharshi Basu <basumaharshi10@gmail.com> * chore: rename checker files to remove `py` prefix Signed-off-by: Maharshi Basu <basumaharshi10@gmail.com> --------- Signed-off-by: Maharshi Basu <basumaharshi10@gmail.com>
1 parent 0d95cb7 commit b8fde51

18 files changed

+787
-0
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from django.http import HttpResponse
2+
from django.views.decorators.csrf import csrf_exempt
3+
4+
# <expect-error>
5+
@csrf_exempt
6+
def my_view(request):
7+
return HttpResponse('Hello world')
8+
9+
import django
10+
11+
# <expect-error>
12+
@django.views.decorators.csrf.csrf_exempt
13+
def my_view2(request):
14+
return HttpResponse('Hello world')

checkers/python/csrf-exempt.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
language: py
2+
name: csrf-exempt
3+
message: Detected usage of csrf_exempt which indicates no CSRF
4+
category: security
5+
6+
pattern: |
7+
(decorated_definition
8+
(decorator
9+
(identifier) @csrf)
10+
(#eq? @csrf "csrf_exempt")) @csrf-exempt
11+
12+
13+
(decorated_definition
14+
(decorator
15+
(attribute
16+
object: (attribute
17+
object: (attribute
18+
object: (attribute
19+
object: (identifier) @django
20+
attribute: (identifier) @views)
21+
attribute: (identifier) @decorator)
22+
attribute: (identifier) @csrf)
23+
attribute: (identifier) @csrf_exempt))
24+
(#eq? @django "django")
25+
(#eq? @views "views")
26+
(#eq? @decorator "decorators")
27+
(#eq? @csrf "csrf")
28+
(#eq? @csrf_exempt "csrf_exempt")) @csrf-exempt
29+
30+
31+
description: |
32+
The decorator `csrf_exempt` disables CSRF protection, making routes vulnerable to attacks. Instead, define the function without this decorator to prevent unauthorized requests.
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# from https://github.com/apache/airflow/commit/80a3d6ac78c5c13abb8826b9dcbe0529f60fed81/
2+
3+
# -*- coding: utf-8 -*-
4+
#
5+
# Licensed to the Apache Software Foundation (ASF) under one
6+
# or more contributor license agreements. See the NOTICE file
7+
# distributed with this work for additional information
8+
# regarding copyright ownership. The ASF licenses this file
9+
# to you under the Apache License, Version 2.0 (the
10+
# "License"); you may not use this file except in compliance
11+
# with the License. You may obtain a copy of the License at
12+
#
13+
# http://www.apache.org/licenses/LICENSE-2.0
14+
#
15+
# Unless required by applicable law or agreed to in writing,
16+
# software distributed under the License is distributed on an
17+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
18+
# KIND, either express or implied. See the License for the
19+
# specific language governing permissions and limitations
20+
# under the License.
21+
22+
import distributed
23+
import subprocess
24+
import warnings
25+
26+
from airflow import configuration
27+
from airflow.executors.base_executor import BaseExecutor
28+
29+
30+
class DaskExecutor(BaseExecutor):
31+
"""
32+
DaskExecutor submits tasks to a Dask Distributed cluster.
33+
"""
34+
def __init__(self, cluster_address=None):
35+
if cluster_address is None:
36+
cluster_address = configuration.conf.get('dask', 'cluster_address')
37+
if not cluster_address:
38+
raise ValueError(
39+
'Please provide a Dask cluster address in airflow.cfg')
40+
self.cluster_address = cluster_address
41+
# ssl / tls parameters
42+
self.tls_ca = configuration.get('dask', 'tls_ca')
43+
self.tls_key = configuration.get('dask', 'tls_key')
44+
self.tls_cert = configuration.get('dask', 'tls_cert')
45+
super(DaskExecutor, self).__init__(parallelism=0)
46+
47+
def start(self):
48+
if self.tls_ca or self.tls_key or self.tls_cert:
49+
from distributed.security import Security
50+
# <expect-error>
51+
security = Security(
52+
tls_client_key=self.tls_key,
53+
tls_client_cert=self.tls_cert,
54+
tls_ca_file=self.tls_ca,
55+
require_encryption=False,
56+
)
57+
else:
58+
security = None
59+
60+
self.client = distributed.Client(self.cluster_address, security=security)
61+
self.futures = {}
62+
63+
def execute_async(self, key, command, queue=None, executor_config=None):
64+
if queue is not None:
65+
warnings.warn(
66+
'DaskExecutor does not support queues. '
67+
'All tasks will be run in the same cluster'
68+
)
69+
70+
def airflow_run():
71+
return subprocess.check_call(command, shell=True, close_fds=True)
72+
73+
future = self.client.submit(airflow_run, pure=False)
74+
self.futures[future] = key
75+
76+
def _process_future(self, future):
77+
if future.done():
78+
key = self.futures[future]
79+
if future.exception():
80+
self.log.error("Failed to execute task: %s", repr(future.exception()))
81+
self.fail(key)
82+
elif future.cancelled():
83+
self.log.error("Failed to execute task")
84+
self.fail(key)
85+
else:
86+
self.success(key)
87+
self.futures.pop(future)
88+
89+
def sync(self):
90+
# make a copy so futures can be popped during iteration
91+
for future in self.futures.copy():
92+
self._process_future(future)
93+
94+
def end(self):
95+
for future in distributed.as_completed(self.futures.copy()):
96+
self._process_future(future)
97+
98+
def terminate(self):
99+
self.client.cancel(self.futures.keys())
100+
self.end()
101+
102+
def create_security_context(self):
103+
# <expect-error>
104+
return distributed.security.Security(
105+
tls_ca_file="ca.pem",
106+
tls_cert="cert.pem",
107+
require_encryption=False,
108+
)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
language: py
2+
name: distributed-security-required-encryption
3+
message: Detected security context for Dask with `require-encryption` keyword as False
4+
5+
pattern: |
6+
(call
7+
function: (identifier) @security
8+
arguments: (argument_list
9+
(_)*
10+
(keyword_argument
11+
name: (identifier) @reqenc
12+
value: (false))
13+
(_)*)
14+
(#eq? @reqenc "require_encryption")) @distributed-security-required-encryption
15+
16+
(call
17+
function: (attribute
18+
object: (attribute
19+
object: (identifier) @dist
20+
attribute: (identifier) @security_mod)
21+
attribute: (identifier) @security_method)
22+
arguments: (argument_list
23+
(_)*
24+
(keyword_argument
25+
name: (identifier) @reqenc
26+
value: (false))
27+
(_)*)
28+
(#eq? @dist "distributed")
29+
(#eq? @security_mod "security")
30+
(#eq? @security_method "Security")
31+
(#eq? @reqenc "require_encryption")
32+
) @distributed-security-required-encryption
33+
34+
description: |
35+
Setting `require_encryption` as False in Dask security may weaken encryption. Set it to True to ensure data protection.
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
from django.db.models import (
2+
CharField, Expression, Field, FloatField, Lookup, TextField, Value,
3+
)
4+
from django.db.models.expressions import CombinedExpression, Func, Subquery
5+
from django.db.models.functions import Cast, Coalesce
6+
7+
# <no-error>
8+
class Star(CharField):
9+
pass
10+
11+
# <expect-error>
12+
class MoreStar(Star):
13+
pass
14+
15+
# <expect-error>
16+
class Position(Func):
17+
function = 'POSITION'
18+
template = "%(function)s('%(substring)s' in %(expressions)s)"
19+
20+
# todoruleid: extends-custom-expression
21+
def __init__(self, expression, substring):
22+
# substring=substring is a SQL injection vulnerability!
23+
super().__init__(expression, substring=substring)
24+
25+
# <expect-error>
26+
class Aggregate(Func):
27+
template = '%(function)s(%(distinct)s%(expressions)s)'
28+
contains_aggregate = True
29+
name = None
30+
filter_template = '%s FILTER (WHERE %%(filter)s)'
31+
window_compatible = True
32+
allow_distinct = False
33+
34+
def __init__(self, *expressions, distinct=False, filter=None, **extra):
35+
if distinct and not self.allow_distinct:
36+
raise TypeError("%s does not allow distinct." % self.__class__.__name__)
37+
self.distinct = distinct
38+
self.filter = filter
39+
super().__init__(*expressions, **extra)
40+
41+
# <expect-error>
42+
class CastToInteger(Func):
43+
"""
44+
A helper class for casting values to signed integer in database.
45+
"""
46+
function = 'CAST'
47+
template = '%(function)s(%(expressions)s as %(integer_type)s)'
48+
49+
def __init__(self, *expressions, **extra):
50+
super().__init__(*expressions, **extra)
51+
self.extra['integer_type'] = 'INTEGER'
52+
53+
def as_mysql(self, compiler, connection):
54+
self.extra['integer_type'] = 'SIGNED'
55+
return super().as_sql(compiler, connection)
56+
57+
# <expect-error>
58+
class DateFormat(Func):
59+
function = 'DATE_FORMAT'
60+
template = '%(function)s(%(expressions)s, "%(format)s")'
61+
62+
def __init__(self, *expressions, **extra):
63+
strf = extra.pop('format', None)
64+
extra['format'] = strf.replace("%", "%%")
65+
extra['output_field'] = CharField()
66+
super(DateFormat, self).__init__(*expressions, **extra)
67+
68+
# <expect-error>
69+
class StrFtime(Func):
70+
function = 'strftime'
71+
template = '%(function)s("%(format)s", %(expressions)s)'
72+
73+
def __init__(self, *expressions, **extra):
74+
strf = extra.pop('format', None)
75+
extra['format'] = strf.replace("%", "%%")
76+
extra['output_field'] = CharField()
77+
super(StrFtime, self).__init__(*expressions, **extra)
78+
79+
# <expect-error>
80+
class RandomUUID(Func):
81+
template = 'GEN_RANDOM_UUID()'
82+
output_field = UUIDField()
83+
84+
# <expect-error>
85+
class SearchHeadline(Func):
86+
function = 'ts_headline'
87+
template = '%(function)s(%(expressions)s%(options)s)'
88+
output_field = TextField()
89+
90+
def __init__(
91+
self, expression, query, *, config=None, start_sel=None, stop_sel=None,
92+
max_words=None, min_words=None, short_word=None, highlight_all=None,
93+
max_fragments=None, fragment_delimiter=None,
94+
):
95+
if not hasattr(query, 'resolve_expression'):
96+
query = SearchQuery(query)
97+
options = {
98+
'StartSel': start_sel,
99+
'StopSel': stop_sel,
100+
'MaxWords': max_words,
101+
'MinWords': min_words,
102+
'ShortWord': short_word,
103+
'HighlightAll': highlight_all,
104+
'MaxFragments': max_fragments,
105+
'FragmentDelimiter': fragment_delimiter,
106+
}
107+
self.options = {
108+
option: value
109+
for option, value in options.items() if value is not None
110+
}
111+
expressions = (expression, query)
112+
if config is not None:
113+
config = SearchConfig.from_parameter(config)
114+
expressions = (config,) + expressions
115+
super().__init__(*expressions)
116+
117+
# <expect-error>
118+
class _Str(Func):
119+
120+
"""Casts a value to the database's text type."""
121+
122+
function = "CAST"
123+
template = "%(function)s(%(expressions)s as %(db_type)s)"
124+
125+
def __init__(self, expression):
126+
super().__init__(expression, output_field=models.TextField())
127+
128+
def as_sql(self, compiler, connection):
129+
self.extra["db_type"] = self.output_field.db_type(connection)
130+
return super().as_sql(compiler, connection)
131+
132+
# <expect-error>
133+
class SQCount(Subquery):
134+
template = "(SELECT count(*) FROM (%(subquery)s) _count)"
135+
output_field = IntegerField()
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
language: py
2+
name: django-class-custom-extends
3+
message: Detected class extension with custom expression
4+
category: security
5+
severity: warning
6+
7+
pattern: |
8+
(class_definition
9+
name: (identifier)
10+
superclasses: (argument_list
11+
(_)*
12+
(identifier) @superclasses
13+
(_)*)
14+
(#match? @superclasses "^(django\\.db\\.models\\.(expressions\\.)?)?(Func|Expression|Value|DurationValue|RawSQL|Star|Random|Col|Ref|ExpressionList|ExpressionWrapper|When|Case|Window|WindowFrame|RowRange|ValueRange|Subquery|Exists)$")) @django-class-custom-extends
15+
16+
description: |
17+
xtending Django's expression classes can introduce security risks, such as SQL injection, if user input is not properly sanitized. Ensure that any user input is validated and does not directly enter custom expressions.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from Crypto.Ciphers import AES
2+
3+
def bad1():
4+
# <expect-error>
5+
cipher = AES.new("", AES.MODE_CFB, iv)
6+
msg = iv + cipher.encrypt(b'Attack at dawn')
7+
8+
def ok1(key):
9+
# <no-error>
10+
cipher = AES.new(key, AES.MODE_EAX, nonce=nonce)
11+
plaintext = cipher.decrypt(ciphertext)

checkers/python/empty-aes-key.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
language: py
2+
name: empty-aes-key
3+
message: Found empty AES key
4+
category: security
5+
6+
pattern: |
7+
(call
8+
function: (attribute
9+
object: (identifier) @aes
10+
attribute: (identifier) @new)
11+
arguments: (argument_list
12+
.
13+
(string
14+
(string_start)
15+
(string_end))
16+
(_)*)
17+
(#eq? @aes "AES")
18+
(#eq? @new "new")) @empty-aes-key
19+
20+
description: |
21+
Possible empty AES encryption key detected. An empty key weakens encryption and makes data vulnerable to attackers. Use a strong, non-empty key for security.

0 commit comments

Comments
 (0)