rzo1 opened a new issue, #2091:
URL: https://github.com/apache/stormcrawler/issues/2091
## What happens
`HttpProtocol.getProtocolOutput()` copies every response header into the
protocol metadata. When a header name equals `_request.headers_` or
`_response.headers_`, the value is base64 decoded first and then stored under
exactly the key the WARC writer treats as the crawler's own capture record.
`_response.ip_` and `_request.time_` are copied in the same loop without any
name check. The interceptor that legitimately sets these keys is only installed
when `http.store.headers` is true, which is not the default, so with the
default configuration nothing overwrites a value that came from the wire.
## Where
`core/src/main/java/org/apache/stormcrawler/protocol/okhttp/HttpProtocol.java:434-446`
on main; consumers in
`external/warc/src/main/java/org/apache/stormcrawler/warc/WARCRecordFormat.java:349`
and `:372-379` and `WARCRequestRecordFormat.java:54`. Config key:
`http.store.headers`.
```java
if (key.equals(ProtocolResponse.REQUEST_HEADERS_KEY)
|| key.equals(ProtocolResponse.RESPONSE_HEADERS_KEY)) {
value = new String(Base64.getDecoder().decode(value),
StandardCharsets.ISO_8859_1);
}
responsemetadata.addValue(key.toLowerCase(Locale.ROOT), value);
```
## Why it matters
`WARCRecordFormat.format` promotes a record from `resource` to `response`
whenever `_response.headers_` is non-blank and embeds the decoded block
verbatim, and `WARCRequestRecordFormat` writes the `_request.headers_` block as
what the crawler supposedly sent. A fetched server can therefore choose the
contents of records that are read as the crawler's own capture, including raw
CRLF inside them, and can set `WARC-IP-Address` and `WARC-Date`. The archive
consumer, not the crawler, carries the consequence, and
`external/warc/README.md:150` tells WARC users to set `http.store.headers:
true`, which makes the interceptor overwrite the forged values. Deployments
that leave it false are the affected ones. Separately, the decode is unguarded,
so a value that is not valid base64 throws `IllegalArgumentException` out of
`getProtocolOutput` and fails that fetch.
## Reproduction
Save as
`core/src/test/java/org/apache/stormcrawler/protocol/OkHttpReservedHeaderKeysTest.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 jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import org.apache.storm.Config;
import org.apache.stormcrawler.Metadata;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
/** Checks that reserved protocol metadata keys cannot be set by a fetched
server. */
class OkHttpReservedHeaderKeysTest extends AbstractProtocolTest {
private static String b64(String s) {
return
Base64.getEncoder().encodeToString(s.getBytes(StandardCharsets.ISO_8859_1));
}
@Override
protected Handler[] getHandlers() {
return new Handler[] {
new AbstractHandler() {
@Override
public void handle(
String target,
Request baseRequest,
jakarta.servlet.http.HttpServletRequest request,
HttpServletResponse response)
throws IOException {
baseRequest.setHandled(true);
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("text/html");
response.setHeader(
ProtocolResponse.RESPONSE_HEADERS_KEY,
b64("HTTP/1.1 200 OK\r\nContent-Type:
text/plain\r\n\r\n"));
response.setHeader(
ProtocolResponse.REQUEST_HEADERS_KEY,
b64("GET /elsewhere HTTP/1.1\r\nHost:
somewhere.example\r\n\r\n"));
response.setHeader(ProtocolResponse.RESPONSE_IP_KEY,
"10.0.0.5");
response.setHeader(ProtocolResponse.REQUEST_TIME_KEY,
"1");
final byte[] content =
"hello".getBytes(StandardCharsets.UTF_8);
response.setContentLength(content.length);
try (OutputStream out = response.getOutputStream()) {
out.write(content);
}
}
}
};
}
@Test
void responseHeadersCannotSetReservedKeys() 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);
ProtocolResponse response =
protocol.getProtocolOutput("http://localhost:" + HTTP_PORT +
"/", new Metadata());
Metadata md = response.getMetadata();
Assertions.assertNull(
md.getFirstValue(ProtocolResponse.RESPONSE_HEADERS_KEY),
"a response header must not populate the verbatim response
record");
Assertions.assertNull(
md.getFirstValue(ProtocolResponse.REQUEST_HEADERS_KEY),
"a response header must not populate the verbatim request
record");
Assertions.assertNull(
md.getFirstValue(ProtocolResponse.RESPONSE_IP_KEY),
"a response header must not populate the recorded IP
address");
protocol.cleanup();
}
}
```
Run it:
```
mvn -pl core test -Dtest=OkHttpReservedHeaderKeysTest
```
A local Jetty server returns the reserved header names; the test asserts the
intended behaviour and fails on main.
```
[ERROR] OkHttpReservedHeaderKeysTest.responseHeadersCannotSetReservedKeys:82
a response header must not populate the verbatim response record ==> expected:
<null> but was: <HTTP/1.1 200 OK
Content-Type: text/plain
>
```
## Suggested fix
In `getProtocolOutput`, drop incoming response headers whose lowercased name
matches one of the reserved keys (`REQUEST_HEADERS_KEY`,
`RESPONSE_HEADERS_KEY`, `RESPONSE_IP_KEY`, `REQUEST_TIME_KEY`,
`PROTOCOL_VERSIONS_KEY`, `TRIMMED_RESPONSE_KEY`,
`TRIMMED_RESPONSE_REASON_KEY`), or store them under a quarantined prefix.
Remove the base64 decoding of server-supplied values entirely, since only
`HTTPHeadersInterceptor` should ever write these keys and it runs after this
loop. Nothing legitimate sends header names of that shape, so no compatibility
impact is expected.
--
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]