mirror of
https://github.com/github/codeql.git
synced 2026-06-11 16:01:09 +02:00
Compare commits
1 Commits
python/cla
...
copilot/ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
677afff7af |
@@ -0,0 +1,22 @@
|
|||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Security;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
|
||||||
|
public class CertificateValidation
|
||||||
|
{
|
||||||
|
public void Bad()
|
||||||
|
{
|
||||||
|
var handler = new HttpClientHandler();
|
||||||
|
// BAD: the callback always returns true, so every certificate is trusted.
|
||||||
|
handler.ServerCertificateCustomValidationCallback =
|
||||||
|
(request, certificate, chain, errors) => true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Good()
|
||||||
|
{
|
||||||
|
var handler = new HttpClientHandler();
|
||||||
|
// GOOD: the certificate is only trusted when there are no validation errors.
|
||||||
|
handler.ServerCertificateCustomValidationCallback =
|
||||||
|
(request, certificate, chain, errors) => errors == SslPolicyErrors.None;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<!DOCTYPE qhelp PUBLIC
|
||||||
|
"-//Semmle//qhelp//EN"
|
||||||
|
"qhelp.dtd">
|
||||||
|
<qhelp>
|
||||||
|
<overview>
|
||||||
|
<p>
|
||||||
|
A TLS/SSL certificate validation callback that always returns <code>true</code> trusts every certificate,
|
||||||
|
regardless of any validation errors that were detected. This allows an attacker to perform a machine-in-the-middle
|
||||||
|
attack against the application, therefore breaking any security that Transport Layer Security (TLS) provides.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
An attack might look like this:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
<li>The vulnerable program connects to <code>https://example.com</code>.</li>
|
||||||
|
<li>The attacker intercepts this connection and presents a valid, self-signed certificate for <code>https://example.com</code>.</li>
|
||||||
|
<li>The vulnerable program calls the certificate validation callback to check whether it should trust the certificate.</li>
|
||||||
|
<li>The callback ignores the <code>SslPolicyErrors</code> argument and returns <code>true</code>.</li>
|
||||||
|
<li>The vulnerable program accepts the certificate and proceeds with the connection, since the callback indicated that the certificate is trusted.</li>
|
||||||
|
<li>The attacker can now read the data the program sends to <code>https://example.com</code> and/or alter its replies while the program thinks the connection is secure.</li>
|
||||||
|
</ol>
|
||||||
|
</overview>
|
||||||
|
|
||||||
|
<recommendation>
|
||||||
|
<p>
|
||||||
|
Do not use a certificate validation callback that unconditionally returns <code>true</code>.
|
||||||
|
Either rely on the default certificate validation, or implement a callback that inspects the
|
||||||
|
<code>SslPolicyErrors</code> argument and only trusts a specific, known certificate (for example, when
|
||||||
|
using a self-signed certificate that has been explicitly pinned).
|
||||||
|
</p>
|
||||||
|
</recommendation>
|
||||||
|
|
||||||
|
<example>
|
||||||
|
<p>
|
||||||
|
In the first (bad) example, the callback always returns <code>true</code> and therefore trusts any certificate,
|
||||||
|
which allows an attacker to perform a machine-in-the-middle attack. In the second (good) example, the callback
|
||||||
|
returns <code>true</code> only when there are no validation errors.
|
||||||
|
</p>
|
||||||
|
<sample src="AcceptAnyCertificate.cs" />
|
||||||
|
</example>
|
||||||
|
|
||||||
|
<references>
|
||||||
|
<li>Microsoft Learn:
|
||||||
|
<a href="https://learn.microsoft.com/en-us/dotnet/api/system.net.security.remotecertificatevalidationcallback">RemoteCertificateValidationCallback Delegate</a>.</li>
|
||||||
|
<li>Microsoft Learn:
|
||||||
|
<a href="https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca5359">CA5359: Do not disable certificate validation</a>.</li>
|
||||||
|
<li>OWASP:
|
||||||
|
<a href="https://owasp.org/www-community/attacks/Manipulator-in-the-middle_attack">Manipulator-in-the-middle attack</a>.</li>
|
||||||
|
</references>
|
||||||
|
</qhelp>
|
||||||
101
csharp/ql/src/Security Features/CWE-295/AcceptAnyCertificate.ql
Normal file
101
csharp/ql/src/Security Features/CWE-295/AcceptAnyCertificate.ql
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
/**
|
||||||
|
* @name Accepting any TLS certificate during validation
|
||||||
|
* @description A certificate validation callback that always accepts any certificate
|
||||||
|
* allows an attacker to perform a machine-in-the-middle attack.
|
||||||
|
* @kind path-problem
|
||||||
|
* @problem.severity error
|
||||||
|
* @security-severity 7.5
|
||||||
|
* @precision high
|
||||||
|
* @id cs/accept-any-certificate
|
||||||
|
* @tags security
|
||||||
|
* external/cwe/cwe-295
|
||||||
|
*/
|
||||||
|
|
||||||
|
import csharp
|
||||||
|
import semmle.code.csharp.dataflow.DataFlow::DataFlow
|
||||||
|
import AcceptAnyCertificate::PathGraph
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds if `c` always returns `true` and never returns `false`, i.e. it accepts
|
||||||
|
* every input it is given.
|
||||||
|
*/
|
||||||
|
predicate alwaysReturnsTrue(Callable c) {
|
||||||
|
c.getReturnType() instanceof BoolType and
|
||||||
|
// There is at least one returned value, and every returned value is the
|
||||||
|
// constant `true`.
|
||||||
|
forex(Expr ret | c.canReturn(ret) | ret.getValue() = "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A delegate type used as a TLS/SSL certificate validation callback. Such a
|
||||||
|
* delegate returns a `bool` (whether the certificate is trusted) and takes a
|
||||||
|
* `System.Net.Security.SslPolicyErrors` parameter describing any validation
|
||||||
|
* errors that were found. This covers `RemoteCertificateValidationCallback` as
|
||||||
|
* well as the `Func<..., SslPolicyErrors, bool>` callbacks used by, for example,
|
||||||
|
* `HttpClientHandler.ServerCertificateCustomValidationCallback`.
|
||||||
|
*/
|
||||||
|
class CertificateValidationCallbackType extends DelegateType {
|
||||||
|
CertificateValidationCallbackType() {
|
||||||
|
this.getReturnType() instanceof BoolType and
|
||||||
|
this.getAParameter().getType().hasFullyQualifiedName("System.Net.Security", "SslPolicyErrors")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a callable that always accepts any certificate, referenced by the
|
||||||
|
* delegate-producing expression `e`.
|
||||||
|
*/
|
||||||
|
Callable getAcceptingCallable(Expr e) {
|
||||||
|
// A lambda or anonymous method, e.g. `(sender, cert, chain, errors) => true`.
|
||||||
|
result = e and
|
||||||
|
alwaysReturnsTrue(e)
|
||||||
|
or
|
||||||
|
// A method group, e.g. `AcceptAllCertificates`, possibly wrapped in an
|
||||||
|
// (implicit or explicit) delegate creation.
|
||||||
|
result = e.(DelegateCreation).getArgument().(CallableAccess).getTarget() and
|
||||||
|
alwaysReturnsTrue(result)
|
||||||
|
or
|
||||||
|
result = e.(CallableAccess).getTarget() and
|
||||||
|
alwaysReturnsTrue(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
module AcceptAnyCertificateConfig implements DataFlow::ConfigSig {
|
||||||
|
predicate isSource(DataFlow::Node source) {
|
||||||
|
exists(getAcceptingCallable(source.asExpr()))
|
||||||
|
or
|
||||||
|
// `HttpClientHandler.DangerousAcceptAnyServerCertificateValidator` is a
|
||||||
|
// built-in callback that accepts every certificate.
|
||||||
|
source
|
||||||
|
.asExpr()
|
||||||
|
.(PropertyAccess)
|
||||||
|
.getTarget()
|
||||||
|
.hasName("DangerousAcceptAnyServerCertificateValidator")
|
||||||
|
}
|
||||||
|
|
||||||
|
predicate isSink(DataFlow::Node sink) {
|
||||||
|
// The value assigned to a property, field or local of certificate
|
||||||
|
// validation callback type.
|
||||||
|
exists(Assignable a |
|
||||||
|
a.getType() instanceof CertificateValidationCallbackType and
|
||||||
|
sink.asExpr() = a.getAnAssignedValue()
|
||||||
|
)
|
||||||
|
or
|
||||||
|
// The value passed as a certificate validation callback argument, e.g. to
|
||||||
|
// the `SslStream` constructor.
|
||||||
|
exists(Call call, Parameter p |
|
||||||
|
p = call.getTarget().getAParameter() and
|
||||||
|
p.getType() instanceof CertificateValidationCallbackType and
|
||||||
|
sink.asExpr() = call.getArgumentForParameter(p)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
predicate observeDiffInformedIncrementalMode() { any() }
|
||||||
|
}
|
||||||
|
|
||||||
|
module AcceptAnyCertificate = DataFlow::Global<AcceptAnyCertificateConfig>;
|
||||||
|
|
||||||
|
from AcceptAnyCertificate::PathNode source, AcceptAnyCertificate::PathNode sink
|
||||||
|
where AcceptAnyCertificate::flowPath(source, sink)
|
||||||
|
select sink.getNode(), source, sink,
|
||||||
|
"This TLS certificate validation $@, which trusts any certificate.", source.getNode(),
|
||||||
|
"uses a callback"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
category: newQuery
|
||||||
|
---
|
||||||
|
* Added a new query, `cs/accept-any-certificate`, to detect TLS/SSL certificate validation callbacks that always accept any certificate (CWE-295).
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
edges
|
||||||
|
| Test.cs:64:45:64:52 | access to local variable callback : (...) => ... | Test.cs:67:48:67:55 | access to local variable callback | provenance | |
|
||||||
|
| Test.cs:65:13:65:56 | (...) => ... : (...) => ... | Test.cs:64:45:64:52 | access to local variable callback : (...) => ... | provenance | |
|
||||||
|
nodes
|
||||||
|
| Test.cs:14:13:14:57 | (...) => ... | semmle.label | (...) => ... |
|
||||||
|
| Test.cs:22:13:25:13 | (...) => ... | semmle.label | (...) => ... |
|
||||||
|
| Test.cs:33:13:33:74 | access to property DangerousAcceptAnyServerCertificateValidator | semmle.label | access to property DangerousAcceptAnyServerCertificateValidator |
|
||||||
|
| Test.cs:40:13:40:56 | (...) => ... | semmle.label | (...) => ... |
|
||||||
|
| Test.cs:52:67:52:75 | delegate creation of type RemoteCertificateValidationCallback | semmle.label | delegate creation of type RemoteCertificateValidationCallback |
|
||||||
|
| Test.cs:59:13:59:56 | (...) => ... | semmle.label | (...) => ... |
|
||||||
|
| Test.cs:64:45:64:52 | access to local variable callback : (...) => ... | semmle.label | access to local variable callback : (...) => ... |
|
||||||
|
| Test.cs:65:13:65:56 | (...) => ... | semmle.label | (...) => ... |
|
||||||
|
| Test.cs:65:13:65:56 | (...) => ... : (...) => ... | semmle.label | (...) => ... : (...) => ... |
|
||||||
|
| Test.cs:67:48:67:55 | access to local variable callback | semmle.label | access to local variable callback |
|
||||||
|
subpaths
|
||||||
|
#select
|
||||||
|
| Test.cs:14:13:14:57 | (...) => ... | Test.cs:14:13:14:57 | (...) => ... | Test.cs:14:13:14:57 | (...) => ... | This TLS certificate validation $@, which trusts any certificate. | Test.cs:14:13:14:57 | (...) => ... | uses a callback |
|
||||||
|
| Test.cs:22:13:25:13 | (...) => ... | Test.cs:22:13:25:13 | (...) => ... | Test.cs:22:13:25:13 | (...) => ... | This TLS certificate validation $@, which trusts any certificate. | Test.cs:22:13:25:13 | (...) => ... | uses a callback |
|
||||||
|
| Test.cs:33:13:33:74 | access to property DangerousAcceptAnyServerCertificateValidator | Test.cs:33:13:33:74 | access to property DangerousAcceptAnyServerCertificateValidator | Test.cs:33:13:33:74 | access to property DangerousAcceptAnyServerCertificateValidator | This TLS certificate validation $@, which trusts any certificate. | Test.cs:33:13:33:74 | access to property DangerousAcceptAnyServerCertificateValidator | uses a callback |
|
||||||
|
| Test.cs:40:13:40:56 | (...) => ... | Test.cs:40:13:40:56 | (...) => ... | Test.cs:40:13:40:56 | (...) => ... | This TLS certificate validation $@, which trusts any certificate. | Test.cs:40:13:40:56 | (...) => ... | uses a callback |
|
||||||
|
| Test.cs:52:67:52:75 | delegate creation of type RemoteCertificateValidationCallback | Test.cs:52:67:52:75 | delegate creation of type RemoteCertificateValidationCallback | Test.cs:52:67:52:75 | delegate creation of type RemoteCertificateValidationCallback | This TLS certificate validation $@, which trusts any certificate. | Test.cs:52:67:52:75 | delegate creation of type RemoteCertificateValidationCallback | uses a callback |
|
||||||
|
| Test.cs:59:13:59:56 | (...) => ... | Test.cs:59:13:59:56 | (...) => ... | Test.cs:59:13:59:56 | (...) => ... | This TLS certificate validation $@, which trusts any certificate. | Test.cs:59:13:59:56 | (...) => ... | uses a callback |
|
||||||
|
| Test.cs:65:13:65:56 | (...) => ... | Test.cs:65:13:65:56 | (...) => ... | Test.cs:65:13:65:56 | (...) => ... | This TLS certificate validation $@, which trusts any certificate. | Test.cs:65:13:65:56 | (...) => ... | uses a callback |
|
||||||
|
| Test.cs:67:48:67:55 | access to local variable callback | Test.cs:65:13:65:56 | (...) => ... : (...) => ... | Test.cs:67:48:67:55 | access to local variable callback | This TLS certificate validation $@, which trusts any certificate. | Test.cs:65:13:65:56 | (...) => ... | uses a callback |
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Security Features/CWE-295/AcceptAnyCertificate.ql
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Security;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
|
||||||
|
public class CertificateValidationTests
|
||||||
|
{
|
||||||
|
public void HttpClientHandlerBad()
|
||||||
|
{
|
||||||
|
var handler = new HttpClientHandler();
|
||||||
|
// BAD: always trusts any certificate.
|
||||||
|
handler.ServerCertificateCustomValidationCallback =
|
||||||
|
(request, certificate, chain, errors) => true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void HttpClientHandlerBlockBodyBad()
|
||||||
|
{
|
||||||
|
var handler = new HttpClientHandler();
|
||||||
|
// BAD: always trusts any certificate.
|
||||||
|
handler.ServerCertificateCustomValidationCallback =
|
||||||
|
(request, certificate, chain, errors) =>
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void HttpClientHandlerDangerousBad()
|
||||||
|
{
|
||||||
|
var handler = new HttpClientHandler();
|
||||||
|
// BAD: built-in callback that accepts any certificate.
|
||||||
|
handler.ServerCertificateCustomValidationCallback =
|
||||||
|
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ServicePointManagerBad()
|
||||||
|
{
|
||||||
|
// BAD: always trusts any certificate.
|
||||||
|
ServicePointManager.ServerCertificateValidationCallback =
|
||||||
|
(sender, certificate, chain, errors) => true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool AcceptAll(object sender, X509Certificate certificate, X509Chain chain,
|
||||||
|
SslPolicyErrors errors)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MethodGroupBad()
|
||||||
|
{
|
||||||
|
// BAD: the referenced method always returns true.
|
||||||
|
ServicePointManager.ServerCertificateValidationCallback = AcceptAll;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SslStreamBad(Stream stream)
|
||||||
|
{
|
||||||
|
// BAD: the validation callback always returns true.
|
||||||
|
var ssl = new SslStream(stream, false,
|
||||||
|
(sender, certificate, chain, errors) => true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void IndirectBad(Stream stream)
|
||||||
|
{
|
||||||
|
RemoteCertificateValidationCallback callback =
|
||||||
|
(sender, certificate, chain, errors) => true;
|
||||||
|
// BAD: the callback flowing here always returns true.
|
||||||
|
var ssl = new SslStream(stream, false, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void HttpClientHandlerGood()
|
||||||
|
{
|
||||||
|
var handler = new HttpClientHandler();
|
||||||
|
// GOOD: the certificate is only trusted when there are no validation errors.
|
||||||
|
handler.ServerCertificateCustomValidationCallback =
|
||||||
|
(request, certificate, chain, errors) => errors == SslPolicyErrors.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool Validate(object sender, X509Certificate certificate, X509Chain chain,
|
||||||
|
SslPolicyErrors errors)
|
||||||
|
{
|
||||||
|
return errors == SslPolicyErrors.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MethodGroupGood()
|
||||||
|
{
|
||||||
|
// GOOD: the referenced method performs real validation.
|
||||||
|
ServicePointManager.ServerCertificateValidationCallback = Validate;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
semmle-extractor-options: /nostdlib /noconfig
|
||||||
|
semmle-extractor-options: --load-sources-from-project:${testdir}/../../../../resources/stubs/_frameworks/Microsoft.NETCore.App/Microsoft.NETCore.App.csproj
|
||||||
@@ -63,7 +63,6 @@ ql/javascript/ql/src/experimental/Security/CWE-347/decodeJwtWithoutVerificationL
|
|||||||
ql/javascript/ql/src/experimental/Security/CWE-444/InsecureHttpParser.ql
|
ql/javascript/ql/src/experimental/Security/CWE-444/InsecureHttpParser.ql
|
||||||
ql/javascript/ql/src/experimental/Security/CWE-522-DecompressionBombs/DecompressionBombs.ql
|
ql/javascript/ql/src/experimental/Security/CWE-522-DecompressionBombs/DecompressionBombs.ql
|
||||||
ql/javascript/ql/src/experimental/Security/CWE-918/SSRF.ql
|
ql/javascript/ql/src/experimental/Security/CWE-918/SSRF.ql
|
||||||
ql/javascript/ql/src/experimental/Security/CWE-918/SsrfIpv6TransitionIncompleteGuard.ql
|
|
||||||
ql/javascript/ql/src/experimental/StandardLibrary/MultipleArgumentsToSetConstructor.ql
|
ql/javascript/ql/src/experimental/StandardLibrary/MultipleArgumentsToSetConstructor.ql
|
||||||
ql/javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-020/UntrustedDataToExternalAPI.ql
|
ql/javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-020/UntrustedDataToExternalAPI.ql
|
||||||
ql/javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-078/CommandInjection.ql
|
ql/javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-078/CommandInjection.ql
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
category: newQuery
|
|
||||||
---
|
|
||||||
* Added a new experimental query, `javascript/ssrf-ipv6-transition-incomplete-guard`, to detect SSRF host-validation guards that reject private IPv4 ranges but fail to unwrap IPv6-transition forms (IPv4-mapped `::ffff:`, NAT64 `64:ff9b::`, 6to4 `2002::`), allowing the guard to be bypassed by wrapping an internal IPv4 address in a transition literal.
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
<!DOCTYPE qhelp PUBLIC
|
|
||||||
"-//Semmle//qhelp//EN"
|
|
||||||
"qhelp.dtd">
|
|
||||||
<qhelp>
|
|
||||||
|
|
||||||
<overview>
|
|
||||||
<p>
|
|
||||||
Server-side request forgery (SSRF) guards frequently reject requests to internal
|
|
||||||
addresses by checking the request host against a denylist of private, loopback and
|
|
||||||
cloud-metadata IPv4 ranges. When such a guard inspects only the dotted-quad IPv4 form
|
|
||||||
and never unwraps IPv6-transition representations, it can be bypassed: the host
|
|
||||||
validator classifies the address as public, but the operating system routes the
|
|
||||||
connection to the embedded internal IPv4 endpoint.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
The affected forms include IPv4-mapped IPv6 (<code>::ffff:169.254.169.254</code>),
|
|
||||||
NAT64 (<code>64:ff9b::a9fe:a9fe</code>) and 6to4 (<code>2002::</code>). A URL such as
|
|
||||||
<code>http://[::ffff:169.254.169.254]/</code> passes a dotted-quad denylist unchanged
|
|
||||||
while still reaching the internal address.
|
|
||||||
</p>
|
|
||||||
</overview>
|
|
||||||
|
|
||||||
<recommendation>
|
|
||||||
<p>
|
|
||||||
Normalize the host before validating it: parse the address with a transition-aware
|
|
||||||
library and unwrap IPv4-mapped, NAT64 and 6to4 forms to their embedded IPv4 address,
|
|
||||||
then apply the private-range check to the normalized value. Libraries such as
|
|
||||||
<code>ipaddr.js</code> classify these forms correctly via their range API, and
|
|
||||||
SSRF-protection libraries such as <code>request-filtering-agent</code> apply the check
|
|
||||||
after DNS resolution. Validate the resolved address rather than the textual host.
|
|
||||||
</p>
|
|
||||||
</recommendation>
|
|
||||||
|
|
||||||
<example>
|
|
||||||
<p>
|
|
||||||
The following guard rejects private IPv4 ranges using the <code>private-ip</code>
|
|
||||||
package, which inspects the textual IPv4 form only. An attacker supplies
|
|
||||||
<code>::ffff:169.254.169.254</code>, which the guard classifies as public, but the
|
|
||||||
request still reaches the internal metadata endpoint.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<sample src="examples/SsrfIpv6TransitionIncompleteGuardBad.js"/>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
The following guard parses the host with a transition-aware classifier, so the
|
|
||||||
embedded internal IPv4 address is detected regardless of the transition form used.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<sample src="examples/SsrfIpv6TransitionIncompleteGuardGood.js"/>
|
|
||||||
</example>
|
|
||||||
|
|
||||||
<references>
|
|
||||||
|
|
||||||
<li>OWASP: <a href="https://owasp.org/www-community/attacks/Server_Side_Request_Forgery">Server-Side Request Forgery</a>.</li>
|
|
||||||
<li>Common Weakness Enumeration: <a href="https://cwe.mitre.org/data/definitions/918.html">CWE-918</a>.</li>
|
|
||||||
<li>Common Weakness Enumeration: <a href="https://cwe.mitre.org/data/definitions/1389.html">CWE-1389</a>.</li>
|
|
||||||
|
|
||||||
</references>
|
|
||||||
</qhelp>
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
/**
|
|
||||||
* @name SSRF host guard does not reject IPv6-transition forms
|
|
||||||
* @description An SSRF host guard that rejects private or loopback IPv4 ranges but never
|
|
||||||
* unwraps IPv6-transition forms (IPv4-mapped `::ffff:`, NAT64 `64:ff9b::`,
|
|
||||||
* 6to4 `2002::`) can be bypassed by wrapping an internal IPv4 address in a
|
|
||||||
* transition literal, allowing requests to reach internal endpoints.
|
|
||||||
* @kind problem
|
|
||||||
* @problem.severity warning
|
|
||||||
* @id javascript/ssrf-ipv6-transition-incomplete-guard
|
|
||||||
* @tags security
|
|
||||||
* experimental
|
|
||||||
* external/cwe/cwe-918
|
|
||||||
* external/cwe/cwe-1389
|
|
||||||
*/
|
|
||||||
|
|
||||||
import javascript
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Holds if `f` imports a dotted-quad-oriented private-IP guard package whose
|
|
||||||
* classification is performed on the textual IPv4 form and therefore returns
|
|
||||||
* `false` for an internal address wrapped in an IPv6-transition literal.
|
|
||||||
*/
|
|
||||||
predicate importsHandRolledIpGuard(File f) {
|
|
||||||
exists(DataFlow::SourceNode mod |
|
|
||||||
mod.getFile() = f and
|
|
||||||
mod = DataFlow::moduleImport(["private-ip", "is-ip", "ip", "ip-range-check"])
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Holds if `f` contains a call to an `isPrivate`-style host classifier, the
|
|
||||||
* common name for a hand-rolled SSRF guard.
|
|
||||||
*/
|
|
||||||
predicate hasIsPrivateCall(File f) {
|
|
||||||
exists(DataFlow::CallNode c |
|
|
||||||
c.getFile() = f and
|
|
||||||
c.getCalleeName().regexpMatch("(?i)^is_?private(ip|address|host)?$")
|
|
||||||
)
|
|
||||||
or
|
|
||||||
exists(DataFlow::MethodCallNode m |
|
|
||||||
m.getFile() = f and
|
|
||||||
m.getMethodName().regexpMatch("(?i)^is_?private(ip|address|host)?$")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Holds if `f` contains a hand-written RFC 1918, loopback or cloud-metadata IPv4
|
|
||||||
* literal used as a denylist entry.
|
|
||||||
*/
|
|
||||||
predicate hasRfc1918Literal(File f) {
|
|
||||||
exists(StringLiteral s |
|
|
||||||
s.getFile() = f and
|
|
||||||
s.getValue()
|
|
||||||
.regexpMatch("(?i).*(127\\.0\\.0\\.1|169\\.254\\.169\\.254|10\\.|192\\.168|172\\.1[6-9]|::1|fc00|fd00|metadata\\.google).*")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Holds if `f` carries any hand-rolled, dotted-quad-oriented SSRF guard signal. */
|
|
||||||
predicate hasUnsafeGuardSignal(File f) {
|
|
||||||
importsHandRolledIpGuard(f) or
|
|
||||||
hasIsPrivateCall(f) or
|
|
||||||
hasRfc1918Literal(f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Holds if `func` has a name that reads as an SSRF host or URL validator. */
|
|
||||||
predicate isSsrfValidatorFunction(Function func) {
|
|
||||||
func.getName()
|
|
||||||
.regexpMatch("(?i).*(validate|check|guard|reject|deny|block|allow|is_?safe|sanitiz)e?_?.*(url|host|ip|address|target|endpoint|webhook|origin).*")
|
|
||||||
or
|
|
||||||
func.getName()
|
|
||||||
.regexpMatch("(?i).*(is_?)?(private|internal|loopback|reserved|external)_?(ip|address|host|url).*")
|
|
||||||
or
|
|
||||||
func.getName().regexpMatch("(?i).*(ssrf|metadata).*")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Holds if `f` imports a maturity-hardened, transition-aware address classifier
|
|
||||||
* or SSRF-protection library that does unwrap IPv6-transition forms.
|
|
||||||
*/
|
|
||||||
predicate importsSafeClassifier(File f) {
|
|
||||||
exists(DataFlow::SourceNode mod |
|
|
||||||
mod.getFile() = f and
|
|
||||||
mod =
|
|
||||||
DataFlow::moduleImport([
|
|
||||||
"ipaddr.js", "ssrf-req-filter", "request-filtering-agent", "ssrf-agent", "netmask",
|
|
||||||
"ip-cidr", "cidr-matcher", "blocked-at"
|
|
||||||
])
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Holds if `f` already performs an explicit IPv6-transition unwrap or
|
|
||||||
* canonicalization, so the guard does see the embedded IPv4 address.
|
|
||||||
*/
|
|
||||||
predicate hasTransitionUnwrap(File f) {
|
|
||||||
exists(StringLiteral s |
|
|
||||||
s.getFile() = f and
|
|
||||||
(
|
|
||||||
s.getValue().matches("%64:ff9b%") or
|
|
||||||
s.getValue().matches("%::ffff%") or
|
|
||||||
s.getValue().matches("%2002:%") or
|
|
||||||
s.getValue().matches("%2001:%")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
or
|
|
||||||
exists(Identifier id |
|
|
||||||
id.getFile() = f and
|
|
||||||
id.getName()
|
|
||||||
.regexpMatch("(?i).*(ipv4mapped|v4mapped|mappedipv4|ipv4inipv6|embeddedipv4|unwrap.*ip|toipv4|canonicaliz|isipv4compat).*")
|
|
||||||
)
|
|
||||||
or
|
|
||||||
exists(DataFlow::MethodCallNode m | m.getFile() = f and m.getMethodName() = ["range", "kind"])
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Holds if `f` is treated as safe (transition-aware), suppressing the alert. */
|
|
||||||
predicate isSafe(File f) { importsSafeClassifier(f) or hasTransitionUnwrap(f) }
|
|
||||||
|
|
||||||
from Function guard, File f
|
|
||||||
where
|
|
||||||
guard.getFile() = f and
|
|
||||||
isSsrfValidatorFunction(guard) and
|
|
||||||
hasUnsafeGuardSignal(f) and
|
|
||||||
not isSafe(f) and
|
|
||||||
not f.getRelativePath()
|
|
||||||
.regexpMatch("(?i).*/(tests?|specs?|examples?|__tests__|e2e|node_modules)/.*")
|
|
||||||
select guard,
|
|
||||||
"This SSRF host guard rejects private IPv4 ranges but never unwraps IPv6-transition forms " +
|
|
||||||
"(IPv4-mapped '::ffff:', NAT64 '64:ff9b::', 6to4 '2002::'); an attacker can wrap an internal " +
|
|
||||||
"IPv4 address in a transition literal to bypass it and reach internal endpoints."
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
const isPrivate = require('private-ip');
|
|
||||||
const fetch = require('node-fetch');
|
|
||||||
|
|
||||||
// BAD: `private-ip` classifies the textual IPv4 form only, so it returns false
|
|
||||||
// for `::ffff:169.254.169.254`. The guard treats the wrapped internal address as
|
|
||||||
// public, but the request still reaches the metadata endpoint.
|
|
||||||
async function validateUrlHost(host) {
|
|
||||||
if (isPrivate(host)) {
|
|
||||||
throw new Error('blocked private host');
|
|
||||||
}
|
|
||||||
return fetch('http://' + host + '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { validateUrlHost };
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
const ipaddr = require('ipaddr.js');
|
|
||||||
const fetch = require('node-fetch');
|
|
||||||
|
|
||||||
// GOOD: ipaddr.js parses the host and classifies it with `.range()`, which is
|
|
||||||
// transition-aware. `::ffff:169.254.169.254` parses as an IPv4-mapped address and
|
|
||||||
// is reported in the `linkLocal` range, so the guard is complete.
|
|
||||||
async function validateTargetHost(host) {
|
|
||||||
const addr = ipaddr.parse(host);
|
|
||||||
const range = addr.range();
|
|
||||||
if (range === 'private' || range === 'loopback' || range === 'linkLocal') {
|
|
||||||
throw new Error('blocked internal host');
|
|
||||||
}
|
|
||||||
return fetch('http://' + host + '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { validateTargetHost };
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
| bad-private-ip-pkg.js:6:1:11:1 | async f ... '/');\\n} | This SSRF host guard rejects private IPv4 ranges but never unwraps IPv6-transition forms (IPv4-mapped '::ffff:', NAT64 '64:ff9b::', 6to4 '2002::'); an attacker can wrap an internal IPv4 address in a transition literal to bypass it and reach internal endpoints. |
|
|
||||||
| bad-rfc1918-regex.js:5:1:16:1 | functio ... '/');\\n} | This SSRF host guard rejects private IPv4 ranges but never unwraps IPv6-transition forms (IPv4-mapped '::ffff:', NAT64 '64:ff9b::', 6to4 '2002::'); an attacker can wrap an internal IPv4 address in a transition literal to bypass it and reach internal endpoints. |
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
experimental/Security/CWE-918/SsrfIpv6TransitionIncompleteGuard.ql
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
const isPrivate = require('private-ip');
|
|
||||||
const fetch = require('node-fetch');
|
|
||||||
|
|
||||||
// BAD: `private-ip` classifies the textual IPv4 form only. It returns false for
|
|
||||||
// `::ffff:169.254.169.254`, so a transition-wrapped internal address slips past.
|
|
||||||
async function validateUrlHost(host) { // NOT OK
|
|
||||||
if (isPrivate(host)) {
|
|
||||||
throw new Error('blocked private host');
|
|
||||||
}
|
|
||||||
return fetch('http://' + host + '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { validateUrlHost };
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
const http = require('http');
|
|
||||||
|
|
||||||
// BAD: a hand-written RFC 1918 / loopback / metadata denylist matched against the
|
|
||||||
// host string. The embedded IPv4 inside `::ffff:10.0.0.1` is never seen.
|
|
||||||
function checkTargetHost(host) { // NOT OK
|
|
||||||
if (
|
|
||||||
host === '127.0.0.1' ||
|
|
||||||
host === '169.254.169.254' ||
|
|
||||||
host.startsWith('10.') ||
|
|
||||||
host.startsWith('192.168') ||
|
|
||||||
host.startsWith('172.16')
|
|
||||||
) {
|
|
||||||
throw new Error('blocked internal host');
|
|
||||||
}
|
|
||||||
return http.get('http://' + host + '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { checkTargetHost };
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
const http = require('http');
|
|
||||||
|
|
||||||
const IPV4_MAPPED_PREFIX = '::ffff:';
|
|
||||||
|
|
||||||
// OK: this guard uses a hand-rolled denylist, but it first unwraps the
|
|
||||||
// IPv6-transition form, so the embedded IPv4 is normalized before the check.
|
|
||||||
function unwrapMapped(host) {
|
|
||||||
// strip an IPv4-mapped `::ffff:` prefix down to the embedded dotted quad
|
|
||||||
if (host.toLowerCase().startsWith(IPV4_MAPPED_PREFIX)) {
|
|
||||||
return host.slice(IPV4_MAPPED_PREFIX.length);
|
|
||||||
}
|
|
||||||
return host;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPrivateAddress(host) { // OK
|
|
||||||
const h = unwrapMapped(host);
|
|
||||||
return (
|
|
||||||
h === '127.0.0.1' ||
|
|
||||||
h === '169.254.169.254' ||
|
|
||||||
h.startsWith('10.') ||
|
|
||||||
h.startsWith('192.168')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateHost(host) { // OK
|
|
||||||
if (isPrivateAddress(host)) {
|
|
||||||
throw new Error('blocked internal host');
|
|
||||||
}
|
|
||||||
return http.get('http://' + host + '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { validateHost };
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
const ipaddr = require('ipaddr.js');
|
|
||||||
const fetch = require('node-fetch');
|
|
||||||
|
|
||||||
// OK: ipaddr.js parses the address and classifies it with `.range()`, which is
|
|
||||||
// transition-aware. `::ffff:10.0.0.1` parses as an IPv4-mapped address and is
|
|
||||||
// reported in the `private` range, so the guard is complete.
|
|
||||||
async function validateTargetHost(host) { // OK
|
|
||||||
const addr = ipaddr.parse(host);
|
|
||||||
const range = addr.range();
|
|
||||||
if (range === 'private' || range === 'loopback' || range === 'linkLocal') {
|
|
||||||
throw new Error('blocked internal host');
|
|
||||||
}
|
|
||||||
return fetch('http://' + host + '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { validateTargetHost };
|
|
||||||
@@ -7,49 +7,3 @@ cursor.execute("some sql", (42,)) # $ getSql="some sql"
|
|||||||
cursor.executemany("some sql", (42,)) # $ getSql="some sql"
|
cursor.executemany("some sql", (42,)) # $ getSql="some sql"
|
||||||
|
|
||||||
cursor.close()
|
cursor.close()
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Connection stored in a class attribute and accessed via various patterns
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
class WrapperA:
|
|
||||||
def __init__(self):
|
|
||||||
self._conn = dbapi.connect(address="hostname", port=300, user="username", pass_arg="testpass")
|
|
||||||
|
|
||||||
def get_connection(self):
|
|
||||||
return self._conn
|
|
||||||
|
|
||||||
|
|
||||||
# Getter called on a fresh constructor result
|
|
||||||
conn_a1 = WrapperA().get_connection()
|
|
||||||
cursor_a1 = conn_a1.cursor()
|
|
||||||
cursor_a1.execute("some sql", (42,)) # $ MISSING: getSql="some sql"
|
|
||||||
|
|
||||||
# Getter called via a stored wrapper instance
|
|
||||||
wrapper_instance = WrapperA()
|
|
||||||
conn_a2 = wrapper_instance.get_connection()
|
|
||||||
cursor_a2 = conn_a2.cursor()
|
|
||||||
cursor_a2.execute("some sql", (42,)) # $ MISSING: getSql="some sql"
|
|
||||||
|
|
||||||
# Direct attribute access on a fresh constructor result
|
|
||||||
conn_b = WrapperA()._conn
|
|
||||||
cursor_b = conn_b.cursor()
|
|
||||||
cursor_b.execute("some sql", (42,)) # $ MISSING: getSql="some sql"
|
|
||||||
|
|
||||||
|
|
||||||
class WrapperB:
|
|
||||||
"""Stores the connection under a different attribute name."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._hana = dbapi.connect(address="hostname", port=300, user="username", pass_arg="testpass")
|
|
||||||
|
|
||||||
def cursor(self):
|
|
||||||
return self._hana.cursor()
|
|
||||||
|
|
||||||
|
|
||||||
# Direct attribute access on a stored instance (mirrors hdb_con3 in the issue)
|
|
||||||
conn_c = WrapperB()._hana
|
|
||||||
cursor_c = conn_c.cursor()
|
|
||||||
cursor_c.execute("some sql", (42,)) # $ MISSING: getSql="some sql"
|
|
||||||
|
|||||||
Reference in New Issue
Block a user