mirror of
https://github.com/github/codeql.git
synced 2026-06-10 23:41:09 +02:00
Compare commits
1 Commits
main
...
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-522-DecompressionBombs/DecompressionBombs.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/heuristics/ql/src/Security/CWE-020/UntrustedDataToExternalAPI.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 };
|
||||
Reference in New Issue
Block a user