Skip to content

Commit decd51b

Browse files
authored
Merge pull request github#12604 from raulgarciamsft/main
Python: Update `py/azure-storage/unsafe-client-side-encryption-in-use`
2 parents 0202baf + f3937a4 commit decd51b

File tree

4 files changed

+292
-71
lines changed

4 files changed

+292
-71
lines changed
Lines changed: 136 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* @name Unsafe usage of v1 version of Azure Storage client-side encryption.
33
* @description Using version v1 of Azure Storage client-side encryption is insecure, and may enable an attacker to decrypt encrypted data
4-
* @kind problem
4+
* @kind path-problem
55
* @tags security
66
* experimental
77
* cryptography
@@ -12,80 +12,145 @@
1212
*/
1313

1414
import python
15+
import semmle.python.dataflow.new.DataFlow
1516
import semmle.python.ApiGraphs
1617

17-
predicate isUnsafeClientSideAzureStorageEncryptionViaAttributes(Call call, AttrNode node) {
18-
exists(
19-
API::Node n, API::Node n2, Attribute a, AssignStmt astmt, API::Node uploadBlob,
20-
ControlFlowNode ctrlFlowNode, string s
21-
|
22-
s in ["key_encryption_key", "key_resolver_function"] and
23-
n =
24-
API::moduleImport("azure")
25-
.getMember("storage")
26-
.getMember("blob")
27-
.getMember("BlobClient")
28-
.getReturn()
29-
.getMember(s) and
30-
n2 =
31-
API::moduleImport("azure")
32-
.getMember("storage")
33-
.getMember("blob")
34-
.getMember("BlobClient")
35-
.getReturn()
36-
.getMember("upload_blob") and
37-
n.getAValueReachableFromSource().asExpr() = a and
38-
astmt.getATarget() = a and
39-
a.getAFlowNode() = node and
40-
uploadBlob =
41-
API::moduleImport("azure")
42-
.getMember("storage")
43-
.getMember("blob")
44-
.getMember("BlobClient")
45-
.getReturn()
46-
.getMember("upload_blob") and
47-
uploadBlob.getACall().asExpr() = call and
48-
ctrlFlowNode = call.getAFlowNode() and
49-
node.strictlyReaches(ctrlFlowNode) and
50-
node != ctrlFlowNode and
51-
not exists(
52-
AssignStmt astmt2, Attribute a2, AttrNode encryptionVersionSet, StrConst uc,
53-
API::Node encryptionVersion
54-
|
55-
uc = astmt2.getValue() and
56-
uc.getText() in ["'2.0'", "2.0"] and
57-
encryptionVersion =
58-
API::moduleImport("azure")
59-
.getMember("storage")
60-
.getMember("blob")
61-
.getMember("BlobClient")
62-
.getReturn()
63-
.getMember("encryption_version") and
64-
encryptionVersion.getAValueReachableFromSource().asExpr() = a2 and
65-
astmt2.getATarget() = a2 and
66-
a2.getAFlowNode() = encryptionVersionSet and
67-
encryptionVersionSet.strictlyReaches(ctrlFlowNode)
68-
)
69-
)
18+
API::Node getBlobServiceClient(boolean isSource) {
19+
isSource = true and
20+
result =
21+
API::moduleImport("azure")
22+
.getMember("storage")
23+
.getMember("blob")
24+
.getMember("BlobServiceClient")
25+
.getReturn()
26+
or
27+
isSource = true and
28+
result =
29+
API::moduleImport("azure")
30+
.getMember("storage")
31+
.getMember("blob")
32+
.getMember("BlobServiceClient")
33+
.getMember("from_connection_string")
34+
.getReturn()
35+
}
36+
37+
API::CallNode getTransitionToContainerClient() {
38+
result = getBlobServiceClient(_).getMember("get_container_client").getACall()
39+
or
40+
result = getBlobClient(_).getMember("_get_container_client").getACall()
41+
}
42+
43+
API::Node getContainerClient(boolean isSource) {
44+
isSource = false and
45+
result = getTransitionToContainerClient().getReturn()
46+
or
47+
isSource = true and
48+
result =
49+
API::moduleImport("azure")
50+
.getMember("storage")
51+
.getMember("blob")
52+
.getMember("ContainerClient")
53+
.getReturn()
54+
or
55+
isSource = true and
56+
result =
57+
API::moduleImport("azure")
58+
.getMember("storage")
59+
.getMember("blob")
60+
.getMember("ContainerClient")
61+
.getMember(["from_connection_string", "from_container_url"])
62+
.getReturn()
63+
}
64+
65+
API::CallNode getTransitionToBlobClient() {
66+
result = [getBlobServiceClient(_), getContainerClient(_)].getMember("get_blob_client").getACall()
67+
}
68+
69+
API::Node getBlobClient(boolean isSource) {
70+
isSource = false and
71+
result = getTransitionToBlobClient().getReturn()
72+
or
73+
isSource = true and
74+
result =
75+
API::moduleImport("azure")
76+
.getMember("storage")
77+
.getMember("blob")
78+
.getMember("BlobClient")
79+
.getReturn()
80+
or
81+
isSource = true and
82+
result =
83+
API::moduleImport("azure")
84+
.getMember("storage")
85+
.getMember("blob")
86+
.getMember("BlobClient")
87+
.getMember(["from_connection_string", "from_blob_url"])
88+
.getReturn()
89+
}
90+
91+
API::Node anyClient(boolean isSource) {
92+
result in [getBlobServiceClient(isSource), getContainerClient(isSource), getBlobClient(isSource)]
7093
}
7194

72-
predicate isUnsafeClientSideAzureStorageEncryptionViaObjectCreation(Call call, ControlFlowNode node) {
73-
exists(API::Node c, string s, Keyword k | k.getAFlowNode() = node |
74-
c.getACall().asExpr() = call and
75-
c = API::moduleImport("azure").getMember("storage").getMember("blob").getMember(s) and
76-
s in ["ContainerClient", "BlobClient", "BlobServiceClient"] and
77-
k.getArg() = "key_encryption_key" and
78-
k = call.getANamedArg() and
79-
not k.getValue() instanceof None and
80-
not exists(Keyword k2 | k2 = call.getANamedArg() |
81-
k2.getArg() = "encryption_version" and
82-
k2.getValue().(StrConst).getText() in ["'2.0'", "2.0"]
95+
newtype TAzureFlowState =
96+
MkUsesV1Encryption() or
97+
MkUsesNoEncryption()
98+
99+
module AzureBlobClientConfig implements DataFlow::StateConfigSig {
100+
class FlowState = TAzureFlowState;
101+
102+
predicate isSource(DataFlow::Node node, FlowState state) {
103+
state = MkUsesNoEncryption() and
104+
node = anyClient(true).asSource()
105+
}
106+
107+
predicate isBarrier(DataFlow::Node node, FlowState state) {
108+
exists(state) and
109+
exists(DataFlow::AttrWrite attr |
110+
node = anyClient(_).getAValueReachableFromSource() and
111+
attr.accesses(node, "encryption_version") and
112+
attr.getValue().asExpr().(StrConst).getText() in ["'2.0'", "2.0"]
83113
)
84-
)
114+
or
115+
// small optimization to block flow with no encryption out of the post-update node
116+
// for the attribute assignment.
117+
isAdditionalFlowStep(_, MkUsesNoEncryption(), node, MkUsesV1Encryption()) and
118+
state = MkUsesNoEncryption()
119+
}
120+
121+
predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2) {
122+
exists(DataFlow::MethodCallNode call |
123+
call in [getTransitionToContainerClient(), getTransitionToBlobClient()] and
124+
node1 = call.getObject() and
125+
node2 = call
126+
)
127+
}
128+
129+
predicate isAdditionalFlowStep(
130+
DataFlow::Node node1, FlowState state1, DataFlow::Node node2, FlowState state2
131+
) {
132+
node1 = node2.(DataFlow::PostUpdateNode).getPreUpdateNode() and
133+
state1 = MkUsesNoEncryption() and
134+
state2 = MkUsesV1Encryption() and
135+
exists(DataFlow::AttrWrite attr |
136+
node1 = anyClient(_).getAValueReachableFromSource() and
137+
attr.accesses(node1, ["key_encryption_key", "key_resolver_function"])
138+
)
139+
}
140+
141+
predicate isSink(DataFlow::Node node, FlowState state) {
142+
state = MkUsesV1Encryption() and
143+
exists(DataFlow::MethodCallNode call |
144+
call = getBlobClient(_).getMember("upload_blob").getACall() and
145+
node = call.getObject()
146+
)
147+
}
85148
}
86149

87-
from Call call, ControlFlowNode node
88-
where
89-
isUnsafeClientSideAzureStorageEncryptionViaAttributes(call, node) or
90-
isUnsafeClientSideAzureStorageEncryptionViaObjectCreation(call, node)
91-
select node, "Unsafe usage of v1 version of Azure Storage client-side encryption."
150+
module AzureBlobClient = DataFlow::GlobalWithState<AzureBlobClientConfig>;
151+
152+
import AzureBlobClient::PathGraph
153+
154+
from AzureBlobClient::PathNode source, AzureBlobClient::PathNode sink
155+
where AzureBlobClient::flowPath(source, sink)
156+
select sink, source, sink, "Unsafe usage of v1 version of Azure Storage client-side encryption"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
edges
2+
| test.py:0:0:0:0 | ModuleVariableNode for test.BSC | test.py:7:19:7:21 | ControlFlowNode for BSC |
3+
| test.py:0:0:0:0 | ModuleVariableNode for test.BSC | test.py:35:19:35:21 | ControlFlowNode for BSC |
4+
| test.py:0:0:0:0 | ModuleVariableNode for test.BSC | test.py:66:19:66:21 | ControlFlowNode for BSC |
5+
| test.py:3:1:3:3 | GSSA Variable BSC | test.py:0:0:0:0 | ModuleVariableNode for test.BSC |
6+
| test.py:3:7:3:51 | ControlFlowNode for Attribute() | test.py:3:1:3:3 | GSSA Variable BSC |
7+
| test.py:7:19:7:21 | ControlFlowNode for BSC | test.py:8:5:8:15 | ControlFlowNode for blob_client |
8+
| test.py:8:5:8:15 | ControlFlowNode for blob_client | test.py:9:5:9:15 | ControlFlowNode for blob_client |
9+
| test.py:9:5:9:15 | ControlFlowNode for blob_client | test.py:9:5:9:15 | [post] ControlFlowNode for blob_client |
10+
| test.py:9:5:9:15 | [post] ControlFlowNode for blob_client | test.py:11:9:11:19 | ControlFlowNode for blob_client |
11+
| test.py:15:27:15:71 | ControlFlowNode for Attribute() | test.py:16:5:16:23 | ControlFlowNode for blob_service_client |
12+
| test.py:16:5:16:23 | ControlFlowNode for blob_service_client | test.py:17:5:17:23 | ControlFlowNode for blob_service_client |
13+
| test.py:17:5:17:23 | ControlFlowNode for blob_service_client | test.py:17:5:17:23 | [post] ControlFlowNode for blob_service_client |
14+
| test.py:17:5:17:23 | [post] ControlFlowNode for blob_service_client | test.py:21:9:21:19 | ControlFlowNode for blob_client |
15+
| test.py:25:24:25:66 | ControlFlowNode for Attribute() | test.py:26:5:26:20 | ControlFlowNode for container_client |
16+
| test.py:26:5:26:20 | ControlFlowNode for container_client | test.py:27:5:27:20 | ControlFlowNode for container_client |
17+
| test.py:27:5:27:20 | ControlFlowNode for container_client | test.py:27:5:27:20 | [post] ControlFlowNode for container_client |
18+
| test.py:27:5:27:20 | [post] ControlFlowNode for container_client | test.py:31:9:31:19 | ControlFlowNode for blob_client |
19+
| test.py:35:19:35:21 | ControlFlowNode for BSC | test.py:36:5:36:15 | ControlFlowNode for blob_client |
20+
| test.py:36:5:36:15 | ControlFlowNode for blob_client | test.py:37:5:37:15 | ControlFlowNode for blob_client |
21+
| test.py:37:5:37:15 | ControlFlowNode for blob_client | test.py:37:5:37:15 | [post] ControlFlowNode for blob_client |
22+
| test.py:37:5:37:15 | [post] ControlFlowNode for blob_client | test.py:43:9:43:19 | ControlFlowNode for blob_client |
23+
| test.py:66:19:66:21 | ControlFlowNode for BSC | test.py:67:5:67:15 | ControlFlowNode for blob_client |
24+
| test.py:67:5:67:15 | ControlFlowNode for blob_client | test.py:68:5:68:15 | ControlFlowNode for blob_client |
25+
| test.py:68:5:68:15 | ControlFlowNode for blob_client | test.py:68:5:68:15 | [post] ControlFlowNode for blob_client |
26+
| test.py:68:5:68:15 | [post] ControlFlowNode for blob_client | test.py:69:12:69:22 | ControlFlowNode for blob_client |
27+
| test.py:69:12:69:22 | ControlFlowNode for blob_client | test.py:73:10:73:33 | ControlFlowNode for get_unsafe_blob_client() |
28+
| test.py:73:10:73:33 | ControlFlowNode for get_unsafe_blob_client() | test.py:75:9:75:10 | ControlFlowNode for bc |
29+
nodes
30+
| test.py:0:0:0:0 | ModuleVariableNode for test.BSC | semmle.label | ModuleVariableNode for test.BSC |
31+
| test.py:3:1:3:3 | GSSA Variable BSC | semmle.label | GSSA Variable BSC |
32+
| test.py:3:7:3:51 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
33+
| test.py:7:19:7:21 | ControlFlowNode for BSC | semmle.label | ControlFlowNode for BSC |
34+
| test.py:8:5:8:15 | ControlFlowNode for blob_client | semmle.label | ControlFlowNode for blob_client |
35+
| test.py:9:5:9:15 | ControlFlowNode for blob_client | semmle.label | ControlFlowNode for blob_client |
36+
| test.py:9:5:9:15 | [post] ControlFlowNode for blob_client | semmle.label | [post] ControlFlowNode for blob_client |
37+
| test.py:11:9:11:19 | ControlFlowNode for blob_client | semmle.label | ControlFlowNode for blob_client |
38+
| test.py:15:27:15:71 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
39+
| test.py:16:5:16:23 | ControlFlowNode for blob_service_client | semmle.label | ControlFlowNode for blob_service_client |
40+
| test.py:17:5:17:23 | ControlFlowNode for blob_service_client | semmle.label | ControlFlowNode for blob_service_client |
41+
| test.py:17:5:17:23 | [post] ControlFlowNode for blob_service_client | semmle.label | [post] ControlFlowNode for blob_service_client |
42+
| test.py:21:9:21:19 | ControlFlowNode for blob_client | semmle.label | ControlFlowNode for blob_client |
43+
| test.py:25:24:25:66 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
44+
| test.py:26:5:26:20 | ControlFlowNode for container_client | semmle.label | ControlFlowNode for container_client |
45+
| test.py:27:5:27:20 | ControlFlowNode for container_client | semmle.label | ControlFlowNode for container_client |
46+
| test.py:27:5:27:20 | [post] ControlFlowNode for container_client | semmle.label | [post] ControlFlowNode for container_client |
47+
| test.py:31:9:31:19 | ControlFlowNode for blob_client | semmle.label | ControlFlowNode for blob_client |
48+
| test.py:35:19:35:21 | ControlFlowNode for BSC | semmle.label | ControlFlowNode for BSC |
49+
| test.py:36:5:36:15 | ControlFlowNode for blob_client | semmle.label | ControlFlowNode for blob_client |
50+
| test.py:37:5:37:15 | ControlFlowNode for blob_client | semmle.label | ControlFlowNode for blob_client |
51+
| test.py:37:5:37:15 | [post] ControlFlowNode for blob_client | semmle.label | [post] ControlFlowNode for blob_client |
52+
| test.py:43:9:43:19 | ControlFlowNode for blob_client | semmle.label | ControlFlowNode for blob_client |
53+
| test.py:66:19:66:21 | ControlFlowNode for BSC | semmle.label | ControlFlowNode for BSC |
54+
| test.py:67:5:67:15 | ControlFlowNode for blob_client | semmle.label | ControlFlowNode for blob_client |
55+
| test.py:68:5:68:15 | ControlFlowNode for blob_client | semmle.label | ControlFlowNode for blob_client |
56+
| test.py:68:5:68:15 | [post] ControlFlowNode for blob_client | semmle.label | [post] ControlFlowNode for blob_client |
57+
| test.py:69:12:69:22 | ControlFlowNode for blob_client | semmle.label | ControlFlowNode for blob_client |
58+
| test.py:73:10:73:33 | ControlFlowNode for get_unsafe_blob_client() | semmle.label | ControlFlowNode for get_unsafe_blob_client() |
59+
| test.py:75:9:75:10 | ControlFlowNode for bc | semmle.label | ControlFlowNode for bc |
60+
subpaths
61+
#select
62+
| test.py:11:9:11:19 | ControlFlowNode for blob_client | test.py:3:7:3:51 | ControlFlowNode for Attribute() | test.py:11:9:11:19 | ControlFlowNode for blob_client | Unsafe usage of v1 version of Azure Storage client-side encryption |
63+
| test.py:21:9:21:19 | ControlFlowNode for blob_client | test.py:15:27:15:71 | ControlFlowNode for Attribute() | test.py:21:9:21:19 | ControlFlowNode for blob_client | Unsafe usage of v1 version of Azure Storage client-side encryption |
64+
| test.py:31:9:31:19 | ControlFlowNode for blob_client | test.py:25:24:25:66 | ControlFlowNode for Attribute() | test.py:31:9:31:19 | ControlFlowNode for blob_client | Unsafe usage of v1 version of Azure Storage client-side encryption |
65+
| test.py:43:9:43:19 | ControlFlowNode for blob_client | test.py:3:7:3:51 | ControlFlowNode for Attribute() | test.py:43:9:43:19 | ControlFlowNode for blob_client | Unsafe usage of v1 version of Azure Storage client-side encryption |
66+
| test.py:75:9:75:10 | ControlFlowNode for bc | test.py:3:7:3:51 | ControlFlowNode for Attribute() | test.py:75:9:75:10 | ControlFlowNode for bc | Unsafe usage of v1 version of Azure Storage client-side encryption |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
experimental/Security/CWE-327/Azure/UnsafeUsageOfClientSideEncryptionVersion.ql
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from azure.storage.blob import BlobServiceClient, ContainerClient, BlobClient
2+
3+
BSC = BlobServiceClient.from_connection_string(...)
4+
5+
def unsafe():
6+
# does not set encryption_version to 2.0, default is unsafe
7+
blob_client = BSC.get_blob_client(...)
8+
blob_client.require_encryption = True
9+
blob_client.key_encryption_key = ...
10+
with open("decryptedcontentfile.txt", "rb") as stream:
11+
blob_client.upload_blob(stream) # BAD
12+
13+
14+
def unsafe_setting_on_blob_service_client():
15+
blob_service_client = BlobServiceClient.from_connection_string(...)
16+
blob_service_client.require_encryption = True
17+
blob_service_client.key_encryption_key = ...
18+
19+
blob_client = blob_service_client.get_blob_client(...)
20+
with open("decryptedcontentfile.txt", "rb") as stream:
21+
blob_client.upload_blob(stream)
22+
23+
24+
def unsafe_setting_on_container_client():
25+
container_client = ContainerClient.from_connection_string(...)
26+
container_client.require_encryption = True
27+
container_client.key_encryption_key = ...
28+
29+
blob_client = container_client.get_blob_client(...)
30+
with open("decryptedcontentfile.txt", "rb") as stream:
31+
blob_client.upload_blob(stream)
32+
33+
34+
def potentially_unsafe(use_new_version=False):
35+
blob_client = BSC.get_blob_client(...)
36+
blob_client.require_encryption = True
37+
blob_client.key_encryption_key = ...
38+
39+
if use_new_version:
40+
blob_client.encryption_version = '2.0'
41+
42+
with open("decryptedcontentfile.txt", "rb") as stream:
43+
blob_client.upload_blob(stream) # BAD
44+
45+
46+
def safe():
47+
blob_client = BSC.get_blob_client(...)
48+
blob_client.require_encryption = True
49+
blob_client.key_encryption_key = ...
50+
# GOOD: Must use `encryption_version` set to `2.0`
51+
blob_client.encryption_version = '2.0'
52+
with open("decryptedcontentfile.txt", "rb") as stream:
53+
blob_client.upload_blob(stream) # OK
54+
55+
56+
def safe_different_order():
57+
blob_client: BlobClient = BSC.get_blob_client(...)
58+
blob_client.encryption_version = '2.0'
59+
blob_client.require_encryption = True
60+
blob_client.key_encryption_key = ...
61+
with open("decryptedcontentfile.txt", "rb") as stream:
62+
blob_client.upload_blob(stream) # OK
63+
64+
65+
def get_unsafe_blob_client():
66+
blob_client = BSC.get_blob_client(...)
67+
blob_client.require_encryption = True
68+
blob_client.key_encryption_key = ...
69+
return blob_client
70+
71+
72+
def unsafe_with_calls():
73+
bc = get_unsafe_blob_client()
74+
with open("decryptedcontentfile.txt", "rb") as stream:
75+
bc.upload_blob(stream) # BAD
76+
77+
78+
def get_safe_blob_client():
79+
blob_client = BSC.get_blob_client(...)
80+
blob_client.require_encryption = True
81+
blob_client.key_encryption_key = ...
82+
blob_client.encryption_version = '2.0'
83+
return blob_client
84+
85+
86+
def safe_with_calls():
87+
bc = get_safe_blob_client()
88+
with open("decryptedcontentfile.txt", "rb") as stream:
89+
bc.upload_blob(stream) # OK

0 commit comments

Comments
 (0)