Search code examples
servletsjettyweb.xml

Excluding HTTP methods using security constraint, jetty


I am trying to prevent all http methods other than GET, PUT and POST accessing my web app with this below security-constraint but it forbids all HTTP methods, even the methods omitted from this constraint.

    <security-constraint>
    <web-resource-collection>
        <web-resource-name>Disable everything</web-resource-name>
        <url-pattern>/</url-pattern>
        <http-method-omission>GET</http-method-omission>
        <http-method-omission>PUT</http-method-omission>
        <http-method-omission>POST</http-method-omission>
    </web-resource-collection>
    <auth-constraint/>
</security-constraint>

However, it works as expected if I include <http-method> list individual methods to be prevented, but I am wondering why did my original constraint didn't work.


Solution

  • Per the Servlet XSD ...

    <http-method>

    Each http-method names an HTTP method to which the constraint applies.

    <http-method-omission>

    Each http-method-omission names an HTTP method to which the constraint does not apply.

    Usually you use both, but in different constraints.

    Take this example (from the Jetty webdefault.xml)

      <security-constraint>
        <web-resource-collection>
          <web-resource-name>Disable TRACE</web-resource-name>
          <url-pattern>/</url-pattern>
          <http-method>TRACE</http-method>
        </web-resource-collection>
        <auth-constraint/>
      </security-constraint>
      <security-constraint>
        <web-resource-collection>
          <web-resource-name>Enable everything but TRACE</web-resource-name>
          <url-pattern>/</url-pattern>
          <http-method-omission>TRACE</http-method-omission>
        </web-resource-collection>
      </security-constraint>
    

    The first constraint rejects TRACE (with an empty auth-constraint).
    The second constraint enables all methods but TRACE.

    Make sure you are using a recent version of Jetty, as there is a omitted method merge bug in versions prior to 9.4.38 (it only works on root context).

    https://github.com/eclipse/jetty.project/issues/5909

    I would recommend reading that issue, as there's plenty of suggestions on behaviors depending on context-path (or not), root url-pattern, default url-pattern, context based url-patterns, etc. Even configurations that replace the webdefault.xml for your specific context.

    Are you using Embedded Jetty?

    If so, you might find using an HttpConfiguration.Customizer easier.

    package jetty;
    
    import java.io.IOException;
    import java.net.URI;
    import java.net.http.HttpClient;
    import java.net.http.HttpRequest;
    import java.net.http.HttpResponse;
    import java.util.EnumSet;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.eclipse.jetty.http.HttpMethod;
    import org.eclipse.jetty.http.HttpStatus;
    import org.eclipse.jetty.server.Connector;
    import org.eclipse.jetty.server.HttpConfiguration;
    import org.eclipse.jetty.server.HttpConnectionFactory;
    import org.eclipse.jetty.server.Request;
    import org.eclipse.jetty.server.Server;
    import org.eclipse.jetty.server.ServerConnector;
    import org.eclipse.jetty.server.handler.AbstractHandler;
    import org.eclipse.jetty.server.handler.DefaultHandler;
    import org.eclipse.jetty.server.handler.HandlerList;
    
    public class RejectHttpMethodsDemo
    {
        public static class BanHttpMethods implements HttpConfiguration.Customizer
        {
            private final EnumSet<HttpMethod> bannedMethods;
    
            public BanHttpMethods(EnumSet<HttpMethod> bannedMethods)
            {
                this.bannedMethods = bannedMethods;
            }
    
            @Override
            public void customize(Connector connector, HttpConfiguration channelConfig, Request request)
            {
                HttpMethod httpMethod = HttpMethod.fromString(request.getMethod());
                if (bannedMethods.contains(httpMethod))
                {
                    request.setHandled(true);
                    request.getResponse().setStatus(HttpStatus.METHOD_NOT_ALLOWED_405);
                }
            }
        }
    
        public static void main(String[] args) throws Exception
        {
            Server server = new Server();
    
            HttpConfiguration httpConfig = new HttpConfiguration();
            httpConfig.addCustomizer(new BanHttpMethods(EnumSet.of(HttpMethod.TRACE, HttpMethod.MOVE)));
            ServerConnector connector = new ServerConnector(server, new HttpConnectionFactory(httpConfig));
            connector.setPort(9090);
            server.addConnector(connector);
    
            HandlerList handlers = new HandlerList();
            handlers.addHandler(new AbstractHandler()
            {
                @Override
                public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
                {
                    baseRequest.setHandled(true);
                    response.setCharacterEncoding("utf-8");
                    response.setContentType("text/plain");
                    response.getWriter().printf("Hello, You asked to %s %s, that is all%n", baseRequest.getMethod(), baseRequest.getRequestURI());
                }
            });
            handlers.addHandler(new DefaultHandler());
    
            server.setHandler(handlers);
            server.start();
    
            try
            {
                HttpClient httpClient = HttpClient.newHttpClient();
    
                demoRequest(httpClient, server.getURI().resolve("/apple"), "GET");
                demoRequest(httpClient, server.getURI().resolve("/banana"), "TRACE");
                demoRequest(httpClient, server.getURI().resolve("/cherry"), "MOVE");
            }
            catch (Throwable t)
            {
                t.printStackTrace();
            }
            finally
            {
                server.stop();
            }
        }
    
        private static void demoRequest(HttpClient httpClient, URI path, String method)
        {
            try
            {
                HttpRequest httpRequest = HttpRequest.newBuilder(path)
                    .method(method, HttpRequest.BodyPublishers.noBody())
                    .build();
                HttpResponse<String> httpResponse = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());
                System.out.printf("HTTP %s %s Response Status: %d%n", httpRequest.method(), httpRequest.uri(), httpResponse.statusCode());
                System.out.println(httpResponse.body());
            }
            catch (IOException | InterruptedException e)
            {
                e.printStackTrace();
            }
        }
    }