Files
codeql/python/ql/src/Security/CWE-327/Ssl.qll
yoff 265a31dc1d Python: switch dataflow library to new (shared) CFG + SSA
Flips the Python dataflow trunk from the legacy CFG (semmle/python/Flow.qll)
and legacy ESSA SSA (semmle/python/essa/*) to the new shared CFG facade
(semmle.python.controlflow.internal.Cfg) and the new SSA adapter
(semmle.python.dataflow.new.internal.SsaImpl), both introduced
additively in the preceding PRs in this stack.

This is the trunk-flip equivalent of the original draft PR #21894 (kept
around as documentation), rebased on top of the four preparatory PRs:

  P1: Remove AstNode.getAFlowNode() and rewrite callers (#21919).
  P2: Qualify Flow.qll's AST references with Py:: prefix (#21920).
  P3: Add new shared-CFG-backed control flow graph (#21921).
  P4: Add new shared-SSA-backed SSA adapter (#21923).

The Python dataflow library (semmle/python/dataflow/new/) now imports
the new CFG facade and SSA adapter. All CFG-typed predicates
(ControlFlowNode, CallNode, BasicBlock, NameNode, AttrNode, ...) are
qualified with the Cfg:: prefix; SSA references switch from
EssaVariable/EssaDefinition to SsaImpl::Definition/SourceVariable.

GuardNode is redesigned to use the new CFG's outcome-node model
(isAfterTrue / isAfterFalse) instead of the legacy ConditionBlock +
flipped indirection. Only BarrierGuard<...> is preserved as public
API.

Framework files (Bottle, FastApi, Django, Tornado, Pyramid, Stdlib,
...) are updated to take CFG nodes from the new facade.

A handful of dataflow consistency tweaks for the new CFG:
- Augmented-assignment targets are treated as both load and store.
- 'from X import *' produces uncertain SSA writes for unknown names.
- CFG nodes are canonicalised so dataflow does not see equivalent
  pre/post-order pairs as distinct nodes.

Two AST tweaks for the new CFG:
- AstNodeImpl: omit PEP 695 type-parameter names from
  FunctionDefExpr / ClassDefExpr children.
- ImportResolution: drop the legacy essa import.

Test churn (~175 files): reblessed library- and query-test .expected
files reflect slightly different CFG granularity, different toString
output, and a handful of true alert deltas in security queries.

Verification: all 367 lib + src + consistency-queries compile clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-30 15:30:47 +00:00

220 lines
7.4 KiB
Plaintext

/**
* Provides modeling of SSL/TLS functionality of the `ssl` module from the standard library.
* See https://docs.python.org/3.9/library/ssl.html
*/
private import python
private import semmle.python.ApiGraphs
private import semmle.python.controlflow.internal.Cfg as Cfg
import TlsLibraryModel
class SslContextCreation extends ContextCreation, DataFlow::CallCfgNode {
SslContextCreation() { this = API::moduleImport("ssl").getMember("SSLContext").getACall() }
override ProtocolVersion getProtocol() {
exists(DataFlow::Node protocolArg, Ssl ssl |
protocolArg in [this.getArg(0), this.getArgByName("protocol")]
|
protocolArg = ssl.specific_version(result).getAValueReachableFromSource()
or
protocolArg = ssl.unspecific_version().getAValueReachableFromSource() and
// see https://docs.python.org/3/library/ssl.html#id7
result in ["TLSv1", "TLSv1_1", "TLSv1_2", "TLSv1_3"]
)
or
not exists(this.getArg(_)) and
not exists(this.getArgByName(_)) and
// see https://docs.python.org/3/library/ssl.html#id7
result in ["TLSv1", "TLSv1_1", "TLSv1_2", "TLSv1_3"]
}
}
class SslDefaultContextCreation extends ContextCreation {
SslDefaultContextCreation() {
this = API::moduleImport("ssl").getMember("create_default_context").getACall()
}
// Allowed insecure versions are "TLSv1" and "TLSv1_1"
// see https://docs.python.org/3/library/ssl.html#context-creation
override ProtocolVersion getProtocol() { result in ["TLSv1", "TLSv1_1", "TLSv1_2", "TLSv1_3"] }
}
/** Gets a reference to an `ssl.Context` instance. */
API::Node sslContextInstance() {
result = API::moduleImport("ssl").getMember(["SSLContext", "create_default_context"]).getReturn()
}
class WrapSocketCall extends ConnectionCreation, DataFlow::MethodCallNode {
WrapSocketCall() { this = sslContextInstance().getMember("wrap_socket").getACall() }
override DataFlow::Node getContext() { result = this.getObject() }
}
class OptionsAugOr extends ProtocolRestriction, DataFlow::CfgNode {
ProtocolVersion restriction;
OptionsAugOr() {
exists(AugAssign aa, Cfg::AttrNode attr, Expr flag |
aa.getOperation().getOp() instanceof BitOr and
aa.getTarget() = attr.getNode() and
attr.getName() = "options" and
attr.getObject() = node and
flag =
API::moduleImport("ssl")
.getMember("OP_NO_" + restriction)
.getAValueReachableFromSource()
.asExpr() and
(
aa.getValue() = flag
or
impliesBitSet(aa.getValue(), flag, false, false)
)
)
}
override DataFlow::Node getContext() { result = this }
override ProtocolVersion getRestriction() { result = restriction }
}
class OptionsAugAndNot extends ProtocolUnrestriction, DataFlow::CfgNode {
ProtocolVersion restriction;
OptionsAugAndNot() {
exists(AugAssign aa, Cfg::AttrNode attr, Expr flag, UnaryExpr notFlag |
aa.getOperation().getOp() instanceof BitAnd and
aa.getTarget() = attr.getNode() and
attr.getName() = "options" and
attr.getObject() = node and
notFlag.getOp() instanceof Invert and
notFlag.getOperand() = flag and
flag =
API::moduleImport("ssl")
.getMember("OP_NO_" + restriction)
.getAValueReachableFromSource()
.asExpr() and
(
aa.getValue() = notFlag
or
impliesBitSet(aa.getValue(), notFlag, true, true)
)
)
}
override DataFlow::Node getContext() { result = this }
override ProtocolVersion getUnrestriction() { result = restriction }
}
/**
* Holds if
* for every bit, _b_:
* `wholeHasBitSet` represents that _b_ is set in `whole`
* implies
* `partHasBitSet` represents that _b_ is set in `part`
*
* As an example take `whole` = `part1 & part2`. Then
* `impliesBitSet(whole, part1, true, true)` holds
* because for any bit in `whole`, if that bit is set it must also be set in `part1`.
*
* Similarly for `whole` = `part1 | part2`. Here
* `impliesBitSet(whole, part1, false, false)` holds
* because for any bit in `whole`, if that bit is not set, it cannot be set in `part1`.
*/
predicate impliesBitSet(BinaryExpr whole, Expr part, boolean partHasBitSet, boolean wholeHasBitSet) {
whole.getOp() instanceof BitAnd and
(
wholeHasBitSet = true and partHasBitSet = true and part in [whole.getLeft(), whole.getRight()]
or
wholeHasBitSet = true and
impliesBitSet([whole.getLeft(), whole.getRight()], part, partHasBitSet, wholeHasBitSet)
)
or
whole.getOp() instanceof BitOr and
(
wholeHasBitSet = false and partHasBitSet = false and part in [whole.getLeft(), whole.getRight()]
or
wholeHasBitSet = false and
impliesBitSet([whole.getLeft(), whole.getRight()], part, partHasBitSet, wholeHasBitSet)
)
}
class ContextSetVersion extends ProtocolRestriction, ProtocolUnrestriction, DataFlow::CfgNode {
ProtocolVersion restriction;
ContextSetVersion() {
exists(DataFlow::AttrWrite aw |
this = aw.getObject() and
aw.getAttributeName() = "minimum_version" and
aw.getValue() =
API::moduleImport("ssl")
.getMember("TLSVersion")
.getMember(restriction)
.getAValueReachableFromSource()
)
}
override DataFlow::Node getContext() { result = this }
override ProtocolVersion getRestriction() { result.lessThan(restriction) }
override ProtocolVersion getUnrestriction() {
restriction = result or restriction.lessThan(result)
}
}
// class UnspecificSslContextCreation extends SslContextCreation, UnspecificContextCreation {
// // UnspecificSslContextCreation() { library instanceof Ssl }
// override ProtocolVersion getProtocol() {
// result = UnspecificContextCreation.super.getProtocol() and
// // These are turned off by default since Python 3.6
// // see https://docs.python.org/3.6/library/ssl.html#ssl.SSLContext
// not result in ["SSLv2", "SSLv3"]
// }
// }
// class UnspecificSslDefaultContextCreation extends SslDefaultContextCreation {
// // override DataFlow::Node getContext() { result = this }
// // see https://docs.python.org/3/library/ssl.html#ssl.create_default_context
// override ProtocolVersion getProtocol() { result in ["TLSv1", "TLSv1_1", "TLSv1_2", "TLSv1_3"] }
// }
class Ssl extends TlsLibrary {
Ssl() { this = "ssl" }
override string specific_version_name(ProtocolVersion version) { result = "PROTOCOL_" + version }
override string unspecific_version_name() {
result = "PROTOCOL_SSLv23"
or
result = "PROTOCOL_TLS" + ["", "_CLIENT", "_SERVER"]
}
override API::Node version_constants() { result = API::moduleImport("ssl") }
override ContextCreation default_context_creation() {
result instanceof SslDefaultContextCreation
}
override ContextCreation specific_context_creation() { result instanceof SslContextCreation }
override DataFlow::CallCfgNode insecure_connection_creation(ProtocolVersion version) {
result = API::moduleImport("ssl").getMember("wrap_socket").getACall() and
this.specific_version(version).getAValueReachableFromSource() =
result.getArgByName("ssl_version") and
version.isInsecure()
}
override ConnectionCreation connection_creation() { result instanceof WrapSocketCall }
override ProtocolRestriction protocol_restriction() {
result instanceof OptionsAugOr
or
result instanceof ContextSetVersion
}
override ProtocolUnrestriction protocol_unrestriction() {
result instanceof OptionsAugAndNot
or
result instanceof ContextSetVersion
}
}