Skip to content

Commit c3d12d8

Browse files
committed
add support for C++/Qt, and genericize shell execution models for all platforms
1 parent 9959a3c commit c3d12d8

File tree

7 files changed

+369
-66
lines changed

7 files changed

+369
-66
lines changed
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/**
2+
* @name Potentially unguarded protocol handler invocation
3+
* @id tob/cpp/unguarded-protocol-handler
4+
* @description Detects calls to URL protocol handlers with untrusted input that may not be properly validated for dangerous protocols
5+
* @kind path-problem
6+
* @tags security
7+
* external/cwe/cwe-939
8+
* @precision medium
9+
* @problem.severity warning
10+
* @security-severity 6.5
11+
* @group security
12+
*/
13+
14+
import cpp
15+
private import semmle.code.cpp.ir.dataflow.TaintTracking
16+
private import semmle.code.cpp.security.FlowSources
17+
private import semmle.code.cpp.security.CommandExecution
18+
19+
/**
20+
* Generic case: invoke protocol handling through OS's protocol handling utilities. This aligns with CVE-2022-43550.
21+
*/
22+
class ShellProtocolHandler extends SystemFunction {
23+
ShellProtocolHandler() {
24+
// Check if any calls to this function contain protocol handler invocations
25+
exists(FunctionCall call |
26+
call.getTarget() = this and
27+
exists(Expr arg |
28+
arg = call.getArgument(0).getAChild*() and
29+
exists(StringLiteral sl | sl = arg or sl.getParent*() = arg |
30+
sl.getValue()
31+
.regexpMatch("(?i).*(rundll32.*url\\.dll.*FileProtocolHandler|xdg-open|\\bopen\\b).*")
32+
)
33+
)
34+
)
35+
}
36+
37+
string getHandlerType() {
38+
exists(FunctionCall call, StringLiteral sl |
39+
call.getTarget() = this and
40+
sl.getParent*() = call.getArgument(0) and
41+
(
42+
sl.getValue().regexpMatch("(?i).*rundll32.*url\\.dll.*FileProtocolHandler.*") and
43+
result = "rundll32 url.dll,FileProtocolHandler"
44+
or
45+
sl.getValue().regexpMatch("(?i).*xdg-open.*") and
46+
result = "xdg-open"
47+
or
48+
sl.getValue().regexpMatch("(?i).*\\bopen\\b.*") and
49+
result = "open"
50+
)
51+
)
52+
}
53+
}
54+
55+
/**
56+
* Qt's QDesktopServices::openUrl method
57+
*/
58+
class QtProtocolHandler extends FunctionCall {
59+
QtProtocolHandler() { this.getTarget().hasQualifiedName("QDesktopServices", "openUrl") }
60+
61+
Expr getUrlArgument() { result = this.getArgument(0) }
62+
}
63+
64+
/**
65+
* A sanitizer node that represents URL scheme validation
66+
*/
67+
class UrlSchemeValidationSanitizer extends DataFlow::Node {
68+
UrlSchemeValidationSanitizer() {
69+
exists(FunctionCall fc |
70+
fc = this.asExpr() and
71+
(
72+
// String comparison on the untrusted URL
73+
fc.getTarget().getName() =
74+
[
75+
"strcmp", "strncmp", "strcasecmp", "strncasecmp", "strstr", "strcasestr", "_stricmp",
76+
"_strnicmp"
77+
]
78+
or
79+
// Qt QUrl::scheme() comparison - QUrl::scheme() returns QString
80+
// Pattern: url.scheme() == "http" or url.scheme() == "https"
81+
exists(FunctionCall schemeCall |
82+
schemeCall.getTarget().hasQualifiedName("QUrl", "scheme") and
83+
(
84+
// Direct comparison
85+
fc.getTarget().hasName(["operator==", "operator!="]) and
86+
fc.getAnArgument() = schemeCall
87+
or
88+
// QString comparison methods
89+
fc = schemeCall and
90+
exists(FunctionCall qstringCmp |
91+
qstringCmp.getQualifier() = schemeCall and
92+
qstringCmp.getTarget().hasQualifiedName("QString", ["compare", "operator=="])
93+
)
94+
)
95+
)
96+
or
97+
// Qt QString startsWith check for direct URL strings
98+
fc.getTarget().hasQualifiedName("QString", "startsWith")
99+
)
100+
)
101+
}
102+
}
103+
104+
/**
105+
* Configuration for tracking untrusted data to protocol handler invocations
106+
*/
107+
module PotentiallyUnguardedProtocolHandlerConfig implements DataFlow::ConfigSig {
108+
predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
109+
110+
predicate isSink(DataFlow::Node sink) {
111+
// QDesktopServices::openUrl()
112+
exists(QtProtocolHandler call | sink.asExpr() = call.getUrlArgument())
113+
or
114+
// Shell protocol handlers (rundll32, xdg-open, open) via system()/popen()/exec*()
115+
exists(FunctionCall call |
116+
call.getTarget() instanceof ShellProtocolHandler and
117+
sink.asExpr() = call.getArgument(0)
118+
)
119+
}
120+
121+
predicate isBarrier(DataFlow::Node node) { node instanceof UrlSchemeValidationSanitizer }
122+
}
123+
124+
module PotentiallyUnguardedProtocolHandlerFlow =
125+
TaintTracking::Global<PotentiallyUnguardedProtocolHandlerConfig>;
126+
127+
import PotentiallyUnguardedProtocolHandlerFlow::PathGraph
128+
129+
from
130+
PotentiallyUnguardedProtocolHandlerFlow::PathNode source,
131+
PotentiallyUnguardedProtocolHandlerFlow::PathNode sink, FunctionCall call, string callType
132+
where
133+
PotentiallyUnguardedProtocolHandlerFlow::flowPath(source, sink) and
134+
(
135+
exists(QtProtocolHandler qtCall |
136+
call = qtCall and
137+
sink.getNode().asExpr() = qtCall.getUrlArgument() and
138+
callType = "QDesktopServices::openUrl()"
139+
)
140+
or
141+
exists(ShellProtocolHandler shellFunc |
142+
call.getTarget() = shellFunc and
143+
sink.getNode().asExpr() = call.getArgument(0) and
144+
callType = shellFunc.getHandlerType()
145+
)
146+
)
147+
select call, source, sink,
148+
callType + " is called with untrusted input from $@ without proper URL scheme validation.",
149+
source.getNode(), "this source"
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
edges
2+
nodes
3+
subpaths
4+
#select
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
security/PotentiallyUnguardedProtocolHandler/PotentiallyUnguardedProtocolHandler.ql
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Forward declarations - minimal to avoid system header dependencies
2+
extern "C" {
3+
int system(const char *);
4+
int sprintf(char *, const char *, ...);
5+
int strncmp(const char *, const char *, unsigned long);
6+
}
7+
8+
// Mock QString class
9+
struct QString {
10+
const char *data;
11+
QString(const char *s) : data(s) {}
12+
bool operator==(const char *other) const;
13+
};
14+
15+
// Mock Qt-like class for QDesktopServices::openUrl
16+
struct QUrl {
17+
const char *url;
18+
QUrl(const char *s) : url(s) {}
19+
QString scheme() const;
20+
bool startsWith(const char *prefix) const;
21+
};
22+
23+
struct QDesktopServices {
24+
static bool openUrl(const QUrl &url);
25+
};
26+
27+
// Untrusted input sources
28+
extern "C" char *getUserInput();
29+
extern "C" const char *getUrlParam();
30+
31+
// BAD: QDesktopServices::openUrl with untrusted input
32+
void bad1_qt(const char *userUrl) {
33+
QUrl url(userUrl);
34+
QDesktopServices::openUrl(url); // BAD
35+
}
36+
37+
void bad2_qt() {
38+
const char *input = getUrlParam();
39+
QUrl url(input);
40+
QDesktopServices::openUrl(url); // BAD
41+
}
42+
43+
void safe1_qt() {
44+
QUrl url("https://example.com");
45+
QDesktopServices::openUrl(url); // GOOD - no taint
46+
}
47+
48+
void safe2_qt(const char *userUrl) {
49+
if (strncmp(userUrl, "https://", 8) == 0 ||
50+
strncmp(userUrl, "http://", 7) == 0) {
51+
QUrl url(userUrl);
52+
QDesktopServices::openUrl(url); // GOOD
53+
}
54+
}
55+
56+
void safe3_qt(QUrl &url) {
57+
if (url.scheme() == "https" || url.scheme() == "http") {
58+
QDesktopServices::openUrl(url); // GOOD
59+
}
60+
}

0 commit comments

Comments
 (0)