Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit b905a3d

Browse files
committed
Python: Attribute access API
1 parent 7e6fa7b commit b905a3d

4 files changed

Lines changed: 289 additions & 7 deletions

File tree

python/ql/src/experimental/dataflow/TypeTracker.qll

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ private import internal.DataFlowPrivate
66

77
/** Any string that may appear as the name of an attribute or access path. */
88
class AttributeName extends string {
9-
AttributeName() { this = any(Attribute a).getName() }
9+
AttributeName() { this = any(AttrRef a).getAttributeName() }
1010
}
1111

1212
/** Either an attribute name, or the empty string (representing no attribute). */
@@ -115,19 +115,22 @@ predicate returnStep(ReturnNode nodeFrom, Node nodeTo) {
115115
* assignment to `z` inside `bar`, even though this attribute write happens _after_ `bar` is called.
116116
*/
117117
predicate basicStoreStep(Node nodeFrom, Node nodeTo, string attr) {
118-
exists(AttributeAssignment a, Node var |
119-
a.getName() = attr and
120-
simpleLocalFlowStep*(nodeTo, var) and
121-
var.asVar() = a.getInput() and
122-
nodeFrom.asCfgNode() = a.getValue()
118+
exists(AttrWrite a |
119+
a.getAttributeName() = attr and
120+
nodeFrom = a.getValue() and
121+
simpleLocalFlowStep*(nodeTo, a.getObject())
123122
)
124123
}
125124

126125
/**
127126
* Holds if `nodeTo` is the result of accessing the `attr` attribute of `nodeFrom`.
128127
*/
129128
predicate basicLoadStep(Node nodeFrom, Node nodeTo, string attr) {
130-
exists(AttrNode s | nodeTo.asCfgNode() = s and s.getObject(attr) = nodeFrom.asCfgNode())
129+
exists(AttrRead a |
130+
attr = a.getAttributeName() and
131+
nodeFrom = a.getObject() and
132+
nodeTo = a
133+
)
131134
}
132135

133136
/**
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
/** This module provides an API for attribute reads and writes. */
2+
3+
import DataFlowPublic
4+
private import DataFlowPrivate
5+
6+
/**
7+
* A data flow node that reads or writes an attribute of an object.
8+
*
9+
* This abstract base class only knows about the base object on which the attribute is being
10+
* accessed, and the attribute itself, if it is statically inferrable.
11+
*/
12+
abstract class AttrRef extends Node {
13+
/**
14+
* Gets the data flow node corresponding to the object whose attribute is being read or written.
15+
*/
16+
abstract Node getObject();
17+
18+
/**
19+
* Gets the expression control flow node that defines the attribute being accessed. This is
20+
* usually an identifier or literal.
21+
*/
22+
abstract ExprNode getAttributeNameExpr();
23+
24+
/** Holds if this attribute reference may access an attribute named `attrName`. */
25+
predicate mayHaveAttributeName(string attrName) { none() }
26+
27+
/** Gets the name of the attribute being read or written, if it can be determined statically. */
28+
abstract string getAttributeName();
29+
}
30+
31+
/**
32+
* A data flow node that writes an attribute of an object. This includes
33+
* - Simple attribute writes: `object.attr = value`
34+
* - Dynamic attribute writes: `setattr(object, attr, value)`
35+
* - Fields written during class initialization: `class MyClass: attr = value`
36+
*/
37+
abstract class AttrWrite extends AttrRef {
38+
/** Gets the data flow node corresponding to the value that is written to the attribute. */
39+
abstract Node getValue();
40+
}
41+
42+
/** A simple attribute assignment: `object.attr = value`. */
43+
private class AttributeAssignmentAsAttrWrite extends AttrWrite, CfgNode {
44+
DefinitionNode attr_node;
45+
46+
AttributeAssignmentAsAttrWrite() { this = TCfgNode(attr_node) and attr_node instanceof AttrNode }
47+
48+
override Node getValue() { result = TCfgNode(attr_node.(DefinitionNode).getValue()) }
49+
50+
override Node getObject() { result = TCfgNode(attr_node.(AttrNode).getObject()) }
51+
52+
override ExprNode getAttributeNameExpr() {
53+
// Attribute names don't exist as `Node`s in the control flow graph, as they can only ever be
54+
// identifiers, and are therefore represented directly as strings.
55+
// Use `getAttributeName` to access the name of the attribute.
56+
none()
57+
}
58+
59+
override string getAttributeName() { result = attr_node.(AttrNode).getName() }
60+
}
61+
62+
import semmle.python.types.Builtins
63+
64+
/** Represents `CallNode`s that may refer to calls to built-in functions or classes. */
65+
private class BuiltInCallNode extends CallNode {
66+
string name;
67+
68+
BuiltInCallNode() {
69+
// TODO disallow instances where `setattr` may refer to an in-scope variable of that name.
70+
exists(NameNode id | this.getFunction() = id and id.getId() = name and id.isGlobal()) and
71+
name = any(Builtin b).getName()
72+
}
73+
74+
/** Gets the name of the built-in function that is called at this `CallNode` */
75+
string getBuiltinName() { result = name }
76+
}
77+
78+
/**
79+
* Represents a call to the built-ins that handle dynamic inspection and modification of
80+
* attributes: `getattr`, `setattr`, and `hasattr`.
81+
*/
82+
private class BuiltinAttrCallNode extends BuiltInCallNode {
83+
BuiltinAttrCallNode() {
84+
name = "setattr" or
85+
name = "getattr" or
86+
name = "hasattr"
87+
}
88+
89+
/** Gets the control flow node for object on which the attribute is accessed. */
90+
ControlFlowNode getObject() { result in [this.getArg(0), this.getArgByName("object")] }
91+
92+
/**
93+
* Gets the control flow node for the value that is being written to the attribute.
94+
* Only relevant for `setattr` calls.
95+
*/
96+
ControlFlowNode getValue() {
97+
// only valid for `setattr`
98+
name = "setattr" and
99+
result in [this.getArg(2), this.getArgByName("value")]
100+
}
101+
102+
/** Gets the control flow node that defines the name of the attribute being accessed. */
103+
ControlFlowNode getName() { result in [this.getArg(1), this.getArgByName("name")] }
104+
}
105+
106+
/** Represents calls to the built-in `setattr`. */
107+
private class SetAttrCallNode extends BuiltinAttrCallNode {
108+
SetAttrCallNode() { name = "setattr" }
109+
}
110+
111+
/** Represents calls to the built-in `getattr`. */
112+
private class GetAttrCallNode extends BuiltinAttrCallNode {
113+
GetAttrCallNode() { name = "getattr" }
114+
}
115+
116+
/** An attribute assignment using `setattr`, e.g. `setattr(object, attr, value)` */
117+
private class SetAttrCallAsAttrWrite extends AttrWrite, CfgNode {
118+
SetAttrCallNode setattr_call;
119+
120+
SetAttrCallAsAttrWrite() { this = TCfgNode(setattr_call) }
121+
122+
override Node getValue() { result = TCfgNode(setattr_call.getValue()) }
123+
124+
override Node getObject() { result = TCfgNode(setattr_call.getObject()) }
125+
126+
override ExprNode getAttributeNameExpr() { result = TCfgNode(setattr_call.getName()) }
127+
128+
override string getAttributeName() {
129+
// TODO track this back using local flow
130+
exists(StrConst s, Node nodeFrom |
131+
s = nodeFrom.asExpr() and
132+
simpleLocalFlowStep*(nodeFrom, this.getAttributeNameExpr()) and
133+
result = s.getText()
134+
)
135+
}
136+
}
137+
138+
/**
139+
* An attribute assignment via a class field, e.g.
140+
* ```python
141+
* class MyClass:
142+
* attr = value
143+
* ```
144+
* is treated as equivalent to `MyClass.attr = value`.
145+
*/
146+
private class ClassDefinitionAsAttrWrite extends AttrWrite, Node {
147+
ClassExpr cls;
148+
DefinitionNode attr_node;
149+
150+
ClassDefinitionAsAttrWrite() {
151+
attr_node instanceof NameNode and
152+
this.asCfgNode() = attr_node and
153+
attr_node.getScope() = cls.getInnerScope()
154+
}
155+
156+
override Node getValue() { result = TCfgNode(attr_node.getValue()) }
157+
158+
override Node getObject() { result = TCfgNode(cls.getAFlowNode()) }
159+
160+
override ExprNode getAttributeNameExpr() { none() }
161+
162+
override string getAttributeName() { result = attr_node.(NameNode).getId() }
163+
}
164+
165+
/**
166+
* A read of an attribute on an object. This includes
167+
* - Simple attribute reads: `object.attr`
168+
* - Dynamic attribute reads using `getattr`: `getattr(object, attr)`
169+
* - Qualified imports: `from module import attr as name`
170+
*/
171+
abstract class AttrRead extends AttrRef, Node { }
172+
173+
/** A simple attribute read, e.g. `object.attr` */
174+
private class AttributeReadAsAttrRead extends AttrRead, CfgNode {
175+
AttrNode attr_node;
176+
177+
AttributeReadAsAttrRead() { this = TCfgNode(attr_node) }
178+
179+
override Node getObject() { result = TCfgNode(attr_node.getObject()) }
180+
181+
override ExprNode getAttributeNameExpr() {
182+
// Attribute names don't exist as `Node`s in the control flow graph, as they can only ever be
183+
// identifiers, and are therefore represented directly as strings.
184+
// Use `getAttributeName` to access the name of the attribute.
185+
none()
186+
}
187+
188+
override string getAttributeName() { result = attr_node.getName() }
189+
}
190+
191+
/** An attribute read using `getattr`: `getattr(object, attr)` */
192+
private class GetAttrCallAsAttrRead extends AttrRead, CfgNode {
193+
GetAttrCallNode getattr_call;
194+
195+
GetAttrCallAsAttrRead() { this.asCfgNode() = getattr_call }
196+
197+
override Node getObject() { result = TCfgNode(getattr_call.getObject()) }
198+
199+
override ExprNode getAttributeNameExpr() { result = TCfgNode(getattr_call.getName()) }
200+
201+
override string getAttributeName() {
202+
exists(StrConst s, Node nodeFrom |
203+
s = nodeFrom.asExpr() and
204+
simpleLocalFlowStep*(nodeFrom, this.getAttributeNameExpr()) and
205+
result = s.getText()
206+
)
207+
}
208+
}

python/ql/src/experimental/dataflow/internal/DataFlowPublic.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
private import python
66
private import DataFlowPrivate
77
import experimental.dataflow.TypeTracker
8+
import Attributes
89
private import semmle.python.essa.SsaCompute
910

1011
/**

python/ql/test/experimental/dataflow/typetracking/attribute_tests.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,73 @@ def test_incompatible_types():
2929
expects_int(x) # $int=field $f+:str=field
3030
x.field = str("Hello") # $f+:int=field $str=field $f+:int $str
3131
expects_string(x) # $f+:int=field $str=field
32+
33+
34+
# Attributes assigned statically to a class
35+
36+
class MyClass: # $tracked=field
37+
field = tracked # $tracked
38+
39+
lookup = MyClass.field # $tracked $tracked=field
40+
instance = MyClass() # $tracked=field
41+
lookup2 = instance.field # $f-:tracked
42+
43+
## Dynamic attribute access
44+
45+
# Via `getattr`/`setattr`
46+
47+
def setattr_immediate_write():
48+
x = SomeClass() # $tracked=foo
49+
setattr(x,"foo", tracked) # $tracked $tracked=foo
50+
y = x.foo # $tracked $tracked=foo
51+
do_stuff(y) # $tracked
52+
53+
def getattr_immediate_read():
54+
x = SomeClass() # $tracked=foo
55+
x.foo = tracked # $tracked $tracked=foo
56+
y = getattr(x,"foo") # $tracked $tracked=foo
57+
do_stuff(y) # $tracked
58+
59+
def setattr_indirect_write():
60+
attr = "foo"
61+
x = SomeClass() # $tracked=foo
62+
setattr(x, attr, tracked) # $tracked $tracked=foo
63+
y = x.foo # $tracked $tracked=foo
64+
do_stuff(y) # $tracked
65+
66+
def getattr_indirect_read():
67+
attr = "foo"
68+
x = SomeClass() # $tracked=foo
69+
x.foo = tracked # $tracked $tracked=foo
70+
y = getattr(x, attr) #$tracked $tracked=foo
71+
do_stuff(y) # $tracked
72+
73+
# Via `__dict__` -- not currently implemented.
74+
75+
def dunder_dict_immediate_write():
76+
x = SomeClass() # $f-:tracked=foo
77+
x.__dict__["foo"] = tracked # $tracked $f-:tracked=foo
78+
y = x.foo # $f-:tracked $f-:tracked=foo
79+
do_stuff(y) # $f-:tracked
80+
81+
def dunder_dict_immediate_read():
82+
x = SomeClass() # $tracked=foo
83+
x.foo = tracked # $tracked $tracked=foo
84+
y = x.__dict__["foo"] # $f-:tracked $tracked=foo
85+
do_stuff(y) # $f-:tracked
86+
87+
def dunder_dict_indirect_write():
88+
attr = "foo"
89+
x = SomeClass() # $f-:tracked=foo
90+
x.__dict__[attr] = tracked # $tracked $f-:tracked=foo
91+
y = x.foo # $f-:tracked $f-:tracked=foo
92+
do_stuff(y) # $f-:tracked
93+
94+
def dunder_dict_indirect_read():
95+
attr = "foo"
96+
x = SomeClass() # $tracked=foo
97+
x.foo = tracked # $tracked $tracked=foo
98+
y = x.__dict__[attr] # $f-:tracked $tracked=foo
99+
do_stuff(y) # $f-:tracked
100+
101+

0 commit comments

Comments
 (0)