CVE-2018-12537 (Java, published 2018-08-14T19:29Z)
Description: value: In Eclipse Vert.x version 3.0 to 3.5.1, the HttpServer response headers and HttpClient request headers do not filter carriage return and line feed characters from the header value. This allow unfiltered values to inject a new header in the client request or server response.
Severity: MEDIUM (CVSS v3 base score: 5.3)
Weakness: CWE-20 \u2014 Improper Input Validation
The product receives input or data, but it does
        not validate or incorrectly validates that the input has the
        properties that are required to process the data safely and
        correctly.
Repository: https://github.com/eclipse/vert.x
Files changed:
src/main/java/io/vertx/core/http/HttpHeaders.java
src/main/java/io/vertx/core/http/impl/Http2HeadersAdaptor.java
src/main/java/io/vertx/core/http/impl/Http2ServerResponseImpl.java
src/main/java/io/vertx/core/http/impl/HttpClientRequestImpl.java
src/main/java/io/vertx/core/http/impl/HttpUtils.java
src/main/java/io/vertx/core/http/impl/headers/VertxHttpHeaders.java
src/test/java/io/vertx/test/core/HttpTest.java
src/test/java/io/vertx/test/core/VertxHttpHeadersTest.java
Commit message: Http header CR / LF validation - fixes #2470


Vulnerable code:
    return io.netty.handler.codec.http.HttpHeaders.newEntity(value);
import io.netty.channel.ChannelFuture;
import io.vertx.core.VertxException;
import io.vertx.core.http.CaseInsensitiveHeaders;
  private CaseInsensitiveHeaders headers;
      headers = new CaseInsensitiveHeaders();
  private final VertxHttpHeaders.MapEntry head = new VertxHttpHeaders.MapEntry(-1, null, null);
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
    
    

Fixed code:
import io.netty.util.AsciiString;
import io.vertx.core.http.impl.HttpUtils;

    HttpUtils.validateHeader(value);
    return new AsciiString(value);
    HttpUtils.validateHeader(name, value);
    HttpUtils.validateHeader(name, values);
    HttpUtils.validateHeader(name, value);
    HttpUtils.validateHeader(name, values);
    HttpUtils.validateHeader(name, value);
    HttpUtils.validateHeader(name, values);
    HttpUtils.validateHeader(name, value);
    HttpUtils.validateHeader(name, values);
import io.vertx.core.http.impl.headers.VertxHttpHeaders;
  private VertxHttpHeaders headers;
      headers = new VertxHttpHeaders();

  public static void validateHeader(CharSequence name, CharSequence value) {
    validateHeader(name);
    validateHeader(value);
  }

  public static void validateHeader(CharSequence name, Iterable<? extends CharSequence> values) {
    validateHeader(name);
    values.forEach(HttpUtils::validateHeader);
  }

  public static void validateHeader(CharSequence value) {
    for (int i = 0;i < value.length();i++) {
      char c = value.charAt(i);
      if (c == '\r' || c == '\n') {
        throw new IllegalArgumentException("Illegal header character: " + ((int)c));
      }
    }
  }
import io.vertx.core.http.impl.HttpUtils;
  private final VertxHttpHeaders.MapEntry head = new VertxHttpHeaders.MapEntry();
    MapEntry() {
      this.hash = -1;
      this.key = null;
      this.value = null;
    }

    if (!(name instanceof AsciiString)) {
      HttpUtils.validateHeader(name);
    }
    if (!(value instanceof AsciiString)) {
      HttpUtils.validateHeader(value);
    }
import java.util.*;
import java.util.function.BiConsumer;


  @Test
  public void testHttpClientRequestHeadersDontContainCROrLF() throws Exception {
    server.requestHandler(req -> {
      req.headers().forEach(header -> {
        String name = header.getKey();
        switch (name.toLowerCase()) {
          case "host":
          case ":method":
          case ":path":
          case ":scheme":
          case ":authority":
            break;
          default:
            fail("Unexpected header " + name);
        }
      });
      testComplete();
    });
    startServer();
    HttpClientRequest req = client.get(DEFAULT_HTTP_PORT, DEFAULT_HTTP_HOST, DEFAULT_TEST_URI, resp -> {});
    List<BiConsumer<String, String>> list = Arrays.asList(
      req::putHeader,
      req.headers()::set,
      req.headers()::add
    );
    list.forEach(cs -> {
      try {
        req.putHeader("header-name: header-value\r\nanother-header", "another-value");
        fail();
      } catch (IllegalArgumentException e) {
      }
    });
    assertEquals(0, req.headers().size());
    req.end();
    await();
  }

  @Test
  public void testHttpServerResponseHeadersDontContainCROrLF() throws Exception {
    server.requestHandler(req -> {
      List<BiConsumer<String, String>> list = Arrays.asList(
        req.response()::putHeader,
        req.response().headers()::set,
        req.response().headers()::add
      );
      list.forEach(cs -> {
        try {
          cs.accept("header-name: header-value\r\nanother-header", "another-value");
          fail();
        } catch (IllegalArgumentException e) {
        }
      });
      assertEquals(Collections.emptySet(), req.response().headers().names());
      req.response().end();
    });
    startServer();
    client.getNow(DEFAULT_HTTP_PORT, DEFAULT_HTTP_HOST, DEFAULT_TEST_URI, resp -> {
      resp.headers().forEach(header -> {
        String name = header.getKey();
        switch (name.toLowerCase()) {
          case ":status":
          case "content-length":
            break;
          default:
            fail("Unexpected header " + name);
        }
      });
      testComplete();
    });
    await();
  }

import org.junit.Test;

  @Override
  public void testHashMININT() {
    // Does not apply
  }