rzo1 opened a new issue, #2079:
URL: https://github.com/apache/stormcrawler/issues/2079
## What happens
`HttpProtocol.configure()` reads `http.trust.everything` with a default of
`true` and then installs a no-op `X509TrustManager` and a `HostnameVerifier`
that returns `true` for every name. The socket factory comes from a static
`SSLContext.getInstance("SSL")`. Accepting certificates the crawler cannot
validate is a reasonable default for a general web crawl and is documented in
`configuration.adoc:261`, but three things do not follow from it. The
`Authorization` header built from `http.basicauth.user` and
`http.basicauth.password`, any credential in `http.custom.headers`, and
replayed cookies are all sent over that unvalidated connection; the hostname is
not checked even when the certificate chain would otherwise be usable; and
"SSL" is the wrong protocol string for a context that should negotiate TLS.
## Where
`core/src/main/java/org/apache/stormcrawler/protocol/okhttp/HttpProtocol.java:132`,
`:216-227` and `:243-252` on main. Config keys: `http.trust.everything`,
`http.basicauth.user`, `http.basicauth.password`, `http.custom.headers`,
`http.use.cookies`.
```java
trustAllSslContext = SSLContext.getInstance("SSL");
...
if (ConfUtils.getBoolean(conf, "http.trust.everything", true)) {
builder.sslSocketFactory(trustAllSslSocketFactory, (X509TrustManager)
trustAllCerts[0]);
builder.hostnameVerifier(
new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
});
}
```
## Why it matters
An operator who configures `http.basicauth.*` for an authenticated crawl has
the credential sent on connections where the server was not authenticated at
all, so anyone able to answer for that name receives it. The always-true
verifier means a certificate issued for one name is accepted for any other,
which is a second, independent loss of server identity. The key is set in no
shipped YAML and nothing is logged when the trust-all manager is installed, so
operators inherit the setting rather than choosing it.
`SSLContext.getInstance("SSL")` currently still negotiates TLS on supported
JREs, but the string asks for the wrong thing.
## Reproduction
Save as
`core/src/test/java/org/apache/stormcrawler/protocol/OkHttpTrustEverythingTest.java`.
```java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.stormcrawler.protocol;
import java.lang.reflect.Field;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import okhttp3.OkHttpClient;
import org.apache.storm.Config;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
/** Checks the TLS setup of the okhttp protocol implementation. */
class OkHttpTrustEverythingTest {
private static Object getField(Class<?> clazz, Object instance, String
name) throws Exception {
Field f = clazz.getDeclaredField(name);
f.setAccessible(true);
return f.get(instance);
}
@Test
void trustAllContextUsesTls() throws Exception {
SSLContext ctx =
(SSLContext)
getField(
org.apache.stormcrawler.protocol.okhttp.HttpProtocol.class,
null,
"trustAllSslContext");
Assertions.assertEquals(
"TLS", ctx.getProtocol(), "the trust-all SSLContext should
be a TLS context");
}
@Test
void hostnameIsStillVerified() throws Exception {
Config conf = new Config();
conf.put("http.agent.name", "this_is_only_a_test");
org.apache.stormcrawler.protocol.okhttp.HttpProtocol protocol =
new org.apache.stormcrawler.protocol.okhttp.HttpProtocol();
protocol.configure(conf);
OkHttpClient client =
(OkHttpClient)
getField(
org.apache.stormcrawler.protocol.okhttp.HttpProtocol.class,
protocol,
"client");
HostnameVerifier verifier = client.hostnameVerifier();
boolean accepted;
try {
// a verifier that actually checks the name needs the session and
// fails or throws when it is absent; the always-true one does
not
accepted = verifier.verify("host.invalid", null);
} catch (RuntimeException e) {
accepted = false;
}
Assertions.assertFalse(
accepted, "the hostname verifier should not accept any name
unconditionally");
protocol.cleanup();
}
}
```
Run it:
```
mvn -pl core test -Dtest=OkHttpTrustEverythingTest
```
Both tests fail on main; they assert the intended behaviour and become
regression tests after the fix.
```
[ERROR] OkHttpTrustEverythingTest.hostnameIsStillVerified:71 the hostname
verifier should not accept any name unconditionally ==> expected: <false> but
was: <true>
[ERROR] OkHttpTrustEverythingTest.trustAllContextUsesTls:45 the trust-all
SSLContext should be a TLS context ==> expected: <TLS> but was: <SSL>
```
The credential leg has no unit test here because it needs a TLS endpoint
with an untrusted certificate. Manually: point a crawl with
`http.basicauth.user` set at an HTTPS server presenting a self-signed
certificate and observe the `Authorization` header arriving.
## Suggested fix
In `HttpProtocol.configure()`, change the static initialiser to
`SSLContext.getInstance("TLS")` and stop installing the always-true
`HostnameVerifier`; certificate trust and hostname checking are separate
decisions, so give the hostname check its own key if anyone needs to switch it
off. Do not add credential headers (`Authorization` from `http.basicauth.*`,
and custom headers marked as credentials) when the trust-all factory is in use,
or require the operator to opt in per configuration; either way log a WARN when
the trust-all manager is installed. Surface `http.trust.everything` in
`crawler-default.yaml` and the archetype `crawler-conf.yaml` files so the
setting is visible. Flipping the default to `false` belongs in a major release,
with a release note for intranet crawls that rely on self-signed certificates.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]