Skip to content

Commit 41b2357

Browse files
authored
Merge pull request #9673 from geoffw0/stringlengthconflation2
Swift: String length conflation query
2 parents 5991e9b + 9013d56 commit 41b2357

File tree

6 files changed

+144
-5
lines changed

6 files changed

+144
-5
lines changed

swift/ql/lib/codeql/swift/elements/type/Type.qll

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@ private import codeql.swift.generated.type.Type
22

33
class Type extends TypeBase {
44
override string toString() { result = this.getDiagnosticsName() }
5+
6+
string getName() { result = this.getDiagnosticsName() }
57
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<overview>
6+
<p>Using a length value from an <code>NSString</code> in a <code>String</code>, or a count from a <code>String</code> in an <code>NSString</code>, may cause unexpected behavior including (in some cases) buffer overwrites. This is because certain unicode sequences are represented as one character in a <code>String</code> but as a sequence of multiple characters in an <code>NSString</code>. For example, a 'thumbs up' emoji with a skin tone modifier (&#x1F44D;&#x1F3FF;) is represented as U+1F44D (&#x1F44D;) then the modifier U+1F3FF.</p>
7+
8+
</overview>
9+
<recommendation>
10+
11+
<p>Use <code>String.count</code> when working with a <code>String</code>. Use <code>NSString.length</code> when working with an <code>NSString</code>. Do not mix values for lengths and offsets between the two types as they are not compatible measures.</p>
12+
13+
<p>If you need to convert between <code>Range</code> and <code>NSRange</code>, do so directly using the appropriate constructor. Do not attempt to use incompatible length and offset values to accomplish conversion.</p>
14+
15+
</recommendation>
16+
<example>
17+
18+
<p>In the following example, a <code>String</code> is converted to <code>NSString</code>, but a range is created from the <code>String</code> to do some processing on it.</p>
19+
20+
<sample src="StringLengthConflationBad.swift" />
21+
22+
<p>This is dangerous because, if the input contains certain characters, the range computed on the <code>String</code> will be wrong for the <code>NSString</code>. This will lead to incorrect behaviour in the string processing that follows. To fix the problem, we can use <code>NSString.length</code> to create the <code>NSRange</code> instead, as follows:</p>
23+
24+
<sample src="StringLengthConflationGood.swift" />
25+
26+
</example>
27+
<references>
28+
29+
<li>
30+
<a href="https://talk.objc.io/episodes/S01E80-swift-string-vs-nsstring">Swift String vs. NSString</a>
31+
</li>
32+
33+
</references>
34+
</qhelp>
Lines changed: 86 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,97 @@
11
/**
22
* @name String length conflation
3-
* @description TODO
3+
* @description Using a length value from an `NSString` in a `String`, or a count from a `String` in an `NSString`, may cause unexpected behavior.
44
* @kind problem
5-
* @problem.severity TODO
6-
* @security-severity TODO
5+
* @problem.severity error
6+
* @security-severity 7.8
77
* @precision TODO
88
* @id swift/string-length-conflation
99
* @tags security
1010
* external/cwe/cwe-135
1111
*/
1212

1313
import swift
14+
import codeql.swift.dataflow.DataFlow
15+
import DataFlow::PathGraph
1416

15-
select "TODO"
17+
class StringLengthConflationConfiguration extends DataFlow::Configuration {
18+
StringLengthConflationConfiguration() { this = "StringLengthConflationConfiguration" }
19+
20+
override predicate isSource(DataFlow::Node node, string flowstate) {
21+
// result of a call to to `String.count`
22+
exists(MemberRefExpr member |
23+
member.getBaseExpr().getType().getName() = "String" and
24+
member.getMember().(VarDecl).getName() = "count" and
25+
node.asExpr() = member and
26+
flowstate = "String"
27+
)
28+
}
29+
30+
override predicate isSink(DataFlow::Node node, string flowstate) {
31+
// arguments to method calls...
32+
exists(
33+
string className, string methodName, string paramName, ClassDecl c, AbstractFunctionDecl f,
34+
CallExpr call, int arg
35+
|
36+
(
37+
// `NSRange.init`
38+
className = "NSRange" and
39+
methodName = "init(location:length:)" and
40+
paramName = ["location", "length"]
41+
or
42+
// `NSString.character`
43+
className = ["NSString", "NSMutableString"] and
44+
methodName = "character(at:)" and
45+
paramName = "at"
46+
or
47+
// `NSString.character`
48+
className = ["NSString", "NSMutableString"] and
49+
methodName = "substring(from:)" and
50+
paramName = "from"
51+
or
52+
// `NSString.character`
53+
className = ["NSString", "NSMutableString"] and
54+
methodName = "substring(to:)" and
55+
paramName = "to"
56+
or
57+
// `NSMutableString.insert`
58+
className = "NSMutableString" and
59+
methodName = "insert(_:at:)" and
60+
paramName = "at"
61+
) and
62+
c.getName() = className and
63+
c.getAMember() = f and // TODO: will this even work if its defined in a parent class?
64+
call.getFunction().(ApplyExpr).getStaticTarget() = f and
65+
f.getName() = methodName and
66+
f.getParam(arg).getName() = paramName and
67+
call.getArgument(arg).getExpr() = node.asExpr() and
68+
flowstate = "String" // `String` length flowing into `NSString`
69+
)
70+
or
71+
// arguments to function calls...
72+
exists(string funcName, string paramName, CallExpr call, int arg |
73+
// `NSMakeRange`
74+
funcName = "NSMakeRange(_:_:)" and
75+
paramName = ["loc", "len"] and
76+
call.getStaticTarget().getName() = funcName and
77+
call.getStaticTarget().getParam(arg).getName() = paramName and
78+
call.getArgument(arg).getExpr() = node.asExpr() and
79+
flowstate = "String" // `String` length flowing into `NSString`
80+
)
81+
}
82+
}
83+
84+
from
85+
StringLengthConflationConfiguration config, DataFlow::PathNode source, DataFlow::PathNode sink,
86+
string flowstate, string message
87+
where
88+
config.hasFlowPath(source, sink) and
89+
config.isSink(sink.getNode(), flowstate) and
90+
(
91+
flowstate = "String" and
92+
message = "This String length is used in an NSString, but it may not be equivalent."
93+
or
94+
flowstate = "NSString" and
95+
message = "This NSString length is used in a String, but it may not be equivalent."
96+
)
97+
select sink.getNode(), source, sink, message
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
2+
func myFunction(s: String) {
3+
let ns = NSString(string: s)
4+
let nsrange = NSMakeRange(0, s.count) // BAD: String length used in NSMakeRange
5+
6+
// ... use nsrange to process ns
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
2+
func myFunction(s: String) {
3+
let ns = NSString(string: s)
4+
let nsrange = NSMakeRange(0, ns.length) // Fixed: NSString length used in NSMakeRange
5+
6+
// ... use nsrange to process ns
7+
}
Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,8 @@
1-
| TODO |
1+
edges
2+
nodes
3+
| StringLengthConflation.swift:72:33:72:35 | .count | semmle.label | .count |
4+
| StringLengthConflation.swift:78:47:78:49 | .count | semmle.label | .count |
5+
subpaths
6+
#select
7+
| StringLengthConflation.swift:72:33:72:35 | .count | StringLengthConflation.swift:72:33:72:35 | .count | StringLengthConflation.swift:72:33:72:35 | .count | This String length is used in an NSString, but it may not be equivalent. |
8+
| StringLengthConflation.swift:78:47:78:49 | .count | StringLengthConflation.swift:78:47:78:49 | .count | StringLengthConflation.swift:78:47:78:49 | .count | This String length is used in an NSString, but it may not be equivalent. |

0 commit comments

Comments
 (0)