Merge pull request #3839 from luchua-bc/uncaught-servlet-exception

Java: Uncaught servlet exception
This commit is contained in:
Anders Schack-Mulligen
2020-12-02 15:12:59 +01:00
committed by GitHub
12 changed files with 610 additions and 1 deletions

View File

@@ -0,0 +1,38 @@
import java.io.InputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
class UncaughtServletException extends HttpServlet {
// BAD: Uncaught exceptions
{
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
String ip = request.getParameter("srcIP");
InetAddress addr = InetAddress.getByName(ip); //BAD: getByName(String) throws UnknownHostException.
String username = request.getRemoteUser();
Integer.parseInt(username); //BAD: Integer.parse(String) throws RuntimeException.
}
}
// GOOD
{
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
try {
String ip = request.getParameter("srcIP");
InetAddress addr = InetAddress.getByName(ip);
} catch (UnknownHostException uhex) { //GOOD: Catch the subclass exception UnknownHostException of IOException.
uhex.printStackTrace();
}
}
}
// GOOD
{
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
String ip = "10.100.10.81";
InetAddress addr = InetAddress.getByName(ip); // OK: Hard-coded variable value or system property is not controlled by attacker.
}
}
}

View File

@@ -0,0 +1,40 @@
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
<qhelp>
<overview>
<p>
Even though the request-handling methods of <code>Servlet</code> are declared <code>throws IOException, ServletException</code>, it's a bad idea to let such exceptions be thrown. Failure to catch exceptions in a servlet could leave a system in an unexpected state, possibly resulting in denial-of-service attacks, or could lead to exposure of sensitive information because when a servlet throws an exception, the servlet container typically sends debugging information back to the user. That information could be valuable to an attacker.
</p>
</overview>
<recommendation>
<p>
Catch IOExceptions and/or RuntimeExceptions and display custom error messages without stack traces and sensitive information, or configure an <code>error-page</code> in web.xml to display a generic user-friendly message for any uncaught exception.
</p>
</recommendation>
<example>
<p>
In the first and second examples, subclasses of IOException and RuntimeException are not caught, which disclose stack traces. Because user-controlled data is passed to methods that throw, there is an opportunity for an attacker to provoke a stack dump.
</p>
<p>
In the third example, the code catches the exception. In the fourth example, the code is not of concern since the variable cannot be controlled by attackers thus no unexpected exceptions can be thrown.
</p>
<sample src="UncaughtServletException.java" />
</example>
<references>
<li>
CWE:
<a href="https://cwe.mitre.org/data/definitions/600.html">CWE-600: Uncaught Exception in Servlet</a>
</li>
<li>
SonarSource:
<a href="https://rules.sonarsource.com/java/tag/owasp/RSPEC-1989">Exceptions should not be thrown from servlet methods</a>
</li>
<li>
OWASP:
<a href="https://cheatsheetseries.owasp.org/cheatsheets/Error_Handling_Cheat_Sheet.html">Error Handling Cheat Sheet</a>
</li>
</references>
</qhelp>

View File

@@ -0,0 +1,74 @@
/**
* @name Uncaught Servlet Exception
* @description Uncaught exceptions in a servlet could leave a system in an unexpected state, possibly resulting in denial-of-service attacks or the exposure of sensitive information disclosed in stack traces.
* @kind path-problem
* @id java/uncaught-servlet-exception
* @tags security
* external/cwe-600
*/
import java
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.dataflow.TaintTracking
import semmle.code.java.frameworks.Servlets
import semmle.code.xml.WebXML
import DataFlow::PathGraph
/** Holds if a given exception type is caught. */
private predicate exceptionIsCaught(TryStmt t, RefType exType) {
exists(CatchClause cc, LocalVariableDeclExpr v |
t.getACatchClause() = cc and
cc.getVariable() = v and
v.getType().(RefType).getASubtype*() = exType and // Detect the case that a subclass exception is thrown but its parent class is declared in the catch clause.
not exists(
ThrowStmt ts // Detect the edge case that exception is caught then rethrown without processing in a catch clause
|
ts.getEnclosingStmt() = cc.getBlock() and
ts.getExpr() = v.getAnAccess()
)
)
}
/** Servlet methods of `javax.servlet.http.Servlet` and subtypes. */
private predicate isServletMethod(Callable c) {
c.getDeclaringType() instanceof ServletClass and
c.getNumberOfParameters() = 2 and
c.getParameter(1).getType() instanceof ServletResponse and
c.getName() in [
"doGet", "doPost", "doPut", "doDelete", "doHead", "doOptions", "doTrace", "service"
]
}
/** Holds if `web.xml` has an error page configured. */
private predicate hasErrorPage() {
exists(WebErrorPage wep | wep.getPageLocation().getValue() != "")
}
/** Sink of uncaught exceptions, which shall be IO exceptions or runtime exceptions since other exception types must be explicitly caught. */
class UncaughtServletExceptionSink extends DataFlow::ExprNode {
UncaughtServletExceptionSink() {
exists(Method m, MethodAccess ma | ma.getMethod() = m |
isServletMethod(ma.getEnclosingCallable()) and
exists(m.getAThrownExceptionType()) and // The called method might plausibly throw an exception.
ma.getAnArgument() = this.getExpr() and
not exists(TryStmt t |
t.getBlock() = ma.getAnEnclosingStmt() and
exceptionIsCaught(t, m.getAThrownExceptionType())
)
)
}
}
/** Taint configuration of uncaught exceptions caused by user provided data from `RemoteFlowSource` */
class UncaughtServletExceptionConfiguration extends TaintTracking::Configuration {
UncaughtServletExceptionConfiguration() { this = "UncaughtServletException" }
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
override predicate isSink(DataFlow::Node sink) { sink instanceof UncaughtServletExceptionSink }
}
from DataFlow::PathNode source, DataFlow::PathNode sink, UncaughtServletExceptionConfiguration c
where c.hasFlowPath(source, sink) and not hasErrorPage()
select sink.getNode(), source, sink, "$@ flows to here and can throw uncaught exception.",
source.getNode(), "User-provided value"

View File

@@ -130,3 +130,40 @@ class WebListenerClass extends WebXMLElement {
*/
Class getClass() { result.getQualifiedName() = getValue() }
}
/**
* An `<error-page>` element in a `web.xml` file.
*/
class WebErrorPage extends WebXMLElement {
WebErrorPage() { this.getName() = "error-page" }
/**
* Gets the `<exception-type>` element of this `<error-page>`.
*/
WebErrorPageType getPageType() { result = getAChild() }
/**
* Gets the `<location>` element of this `<error-page>`.
*/
WebErrorPageLocation getPageLocation() { result = getAChild() }
}
/**
* An `<exception-type>` element in a `web.xml` file, nested under an `<error-page>` element.
*/
class WebErrorPageType extends WebXMLElement {
WebErrorPageType() {
getName() = "exception-type" and
getParent() instanceof WebErrorPage
}
}
/**
* A `<location>` element in a `web.xml` file, nested under an `<error-page>` element.
*/
class WebErrorPageLocation extends WebXMLElement {
WebErrorPageLocation() {
getName() = "location" and
getParent() instanceof WebErrorPage
}
}

View File

@@ -0,0 +1,19 @@
edges
| UncaughtServletException.java:13:15:13:43 | getParameter(...) : String | UncaughtServletException.java:14:44:14:45 | ip |
| UncaughtServletException.java:16:19:16:41 | getRemoteUser(...) : String | UncaughtServletException.java:17:20:17:25 | userId |
| UncaughtServletException.java:54:16:54:44 | getParameter(...) : String | UncaughtServletException.java:55:45:55:46 | ip |
| UncaughtServletException.java:75:21:75:43 | getRemoteUser(...) : String | UncaughtServletException.java:76:22:76:27 | userId |
nodes
| UncaughtServletException.java:13:15:13:43 | getParameter(...) : String | semmle.label | getParameter(...) : String |
| UncaughtServletException.java:14:44:14:45 | ip | semmle.label | ip |
| UncaughtServletException.java:16:19:16:41 | getRemoteUser(...) : String | semmle.label | getRemoteUser(...) : String |
| UncaughtServletException.java:17:20:17:25 | userId | semmle.label | userId |
| UncaughtServletException.java:54:16:54:44 | getParameter(...) : String | semmle.label | getParameter(...) : String |
| UncaughtServletException.java:55:45:55:46 | ip | semmle.label | ip |
| UncaughtServletException.java:75:21:75:43 | getRemoteUser(...) : String | semmle.label | getRemoteUser(...) : String |
| UncaughtServletException.java:76:22:76:27 | userId | semmle.label | userId |
#select
| UncaughtServletException.java:14:44:14:45 | ip | UncaughtServletException.java:13:15:13:43 | getParameter(...) : String | UncaughtServletException.java:14:44:14:45 | ip | $@ flows to here and can throw uncaught exception. | UncaughtServletException.java:13:15:13:43 | getParameter(...) | User-provided value |
| UncaughtServletException.java:17:20:17:25 | userId | UncaughtServletException.java:16:19:16:41 | getRemoteUser(...) : String | UncaughtServletException.java:17:20:17:25 | userId | $@ flows to here and can throw uncaught exception. | UncaughtServletException.java:16:19:16:41 | getRemoteUser(...) | User-provided value |
| UncaughtServletException.java:55:45:55:46 | ip | UncaughtServletException.java:54:16:54:44 | getParameter(...) : String | UncaughtServletException.java:55:45:55:46 | ip | $@ flows to here and can throw uncaught exception. | UncaughtServletException.java:54:16:54:44 | getParameter(...) | User-provided value |
| UncaughtServletException.java:76:22:76:27 | userId | UncaughtServletException.java:75:21:75:43 | getRemoteUser(...) : String | UncaughtServletException.java:76:22:76:27 | userId | $@ flows to here and can throw uncaught exception. | UncaughtServletException.java:75:21:75:43 | getRemoteUser(...) | User-provided value |

View File

@@ -0,0 +1,106 @@
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletException;
class UncaughtServletException extends HttpServlet {
// BAD - Tests `doGet` without catching exceptions.
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
String ip = request.getParameter("srcIP");
InetAddress addr = InetAddress.getByName(ip); // getByName(String) throws UnknownHostException
String userId = request.getRemoteUser();
Integer.parseInt(userId); // Integer.parse(String) throws RuntimeException
}
// GOOD - Tests `doPost` with catching exceptions.
public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
try {
String ip = request.getParameter("srcIP");
InetAddress addr = InetAddress.getByName(ip);
String userId = request.getRemoteUser();
Integer.parseInt(userId); // Integer.parse(String) throws RuntimeException
} catch (UnknownHostException uhex) {
uhex.printStackTrace();
} catch (RuntimeException re) {
re.printStackTrace();
}
}
// GOOD - Tests `doPut` without user provided data and without catching exceptions.
public void doPut(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
String ip = "10.100.10.81";
InetAddress addr = InetAddress.getByName(ip); // GOOD: hard-coded variable value or system property not controlled by attacker
}
// GOOD - Tests rethrowing caught exceptions without stack trace, which the typical programming practice.
public void doDelete(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
try {
String ip = request.getParameter("srcIP");
InetAddress addr = InetAddress.getByName(ip);
} catch (UnknownHostException uhex) {
throw new IOException("Host not found "+uhex.getMessage());
}
}
// BAD - Tests rethrowing caught exceptions with stack trace.
public void doOptions(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
try {
String ip = request.getParameter("srcIP");
InetAddress addr = InetAddress.getByName(ip);
} catch (UnknownHostException uhex) {
uhex.printStackTrace();
throw uhex;
}
}
// GOOD - Tests invoking another top-level method.
public void doHead(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
doGet(request, response);
}
// BAD - Tests nested try-blocks without catching runtime exceptions.
public void service(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
try {
String ip = request.getParameter("srcIP");
InetAddress addr = null;
try {
addr = InetAddress.getByName(ip);
String userId = request.getRemoteUser();
Integer.parseInt(userId); // Integer.parse(String) throws RuntimeException
} catch (UnknownHostException uhex) {
throw new UnknownHostException("Got exception "+uhex.getMessage());
}
} catch (IOException ie) {
ie.printStackTrace();
}
}
// GOOD - Tests nested try-blocks with catching all exceptions.
public void doTrace(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
try {
try {
String ip = request.getParameter("srcIP");
InetAddress addr = null;
try {
addr = InetAddress.getByName(ip);
String userId = request.getRemoteUser();
Integer.parseInt(userId); // Integer.parse(String) throws RuntimeException
} catch (UnknownHostException uhex) {
throw new UnknownHostException("Got exception "+uhex.getMessage());
}
} catch (IOException ie) {
ie.printStackTrace();
}
} catch (RuntimeException re) {
re.printStackTrace();
}
}
}

View File

@@ -0,0 +1 @@
experimental/Security/CWE/CWE-600/UncaughtServletException.ql

View File

@@ -0,0 +1,59 @@
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletException;
class UncaughtServletException2 extends HttpServlet {
// BAD - Tests rethrowing caught exceptions with stack trace using `initCause(...)`
// Note this special case is not being handled by the query since in 99% of cases we're looking for `catch(Exception e) { ... throw e; }`
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
try {
String ip = request.getParameter("srcIP");
InetAddress addr = InetAddress.getByName(ip);
} catch (UnknownHostException uhex) {
IOException ioException = new IOException();
ioException.initCause(uhex);
throw ioException;
}
}
// BAD - Tests rethrowing caught exceptions with stack trace using the same exception variable.
// Note this special case is not being handled by the query since in 99% of cases we're looking for `catch(Exception e) { ... throw e; }`
public void doHead(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
try {
String ip = request.getParameter("srcIP");
InetAddress addr = InetAddress.getByName(ip);
} catch (UnknownHostException uhex) {
throw new IOException(uhex);
}
}
// BAD - Tests rethrowing caught exceptions with stack trace using `addSuppressed(...)`.
// Note this special case is not being handled by the query since in 99% of cases we're looking for `catch(Exception e) { ... throw e; }`
public void doTrace(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
try {
String ip = request.getParameter("srcIP");
InetAddress addr = InetAddress.getByName(ip);
} catch (UnknownHostException uhex) {
IOException ioException = new IOException();
ioException.addSuppressed(uhex);
throw ioException;
}
}
// BAD - Tests rethrowing caught exceptions with stack trace using `initCause(...)`
// Note this special case is not being handled by the query since in 99% of cases we're looking for `catch(Exception e) { ... throw e; }`
public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
try {
String ip = request.getParameter("srcIP");
InetAddress addr = InetAddress.getByName(ip);
} catch (UnknownHostException uhex) {
IOException ioException = new IOException();
throw new IOException(ioException.initCause(uhex));
}
}
}

View File

@@ -0,0 +1 @@
// semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/servlet-api-2.4

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
id="myapp" version="3.0">
<display-name>myapp</display-name>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
<welcome-file>index.xhtml</welcome-file>
</welcome-file-list>
<!-- error-page>
<location>/index.jsp</location>
</error-page -->
</web-app>

View File

@@ -0,0 +1,216 @@
/**
*
* Copyright 2003-2004 The Apache Software Foundation
*
* Licensed 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.
*/
/*
* Adapted from the Java Servlet API version 2.4 as available at
* http://search.maven.org/remotecontent?filepath=javax/servlet/servlet-api/2.4/servlet-api-2.4-sources.jar
* Only relevant stubs of this file have been retained for test purposes.
*/
package javax.servlet;
import java.io.IOException;
import java.util.Enumeration;
/**
*
* Defines a generic, protocol-independent
* servlet. To write an HTTP servlet for use on the
* Web, extend {@link javax.servlet.http.HttpServlet} instead.
*
* <p><code>GenericServlet</code> implements the <code>Servlet</code>
* and <code>ServletConfig</code> interfaces. <code>GenericServlet</code>
* may be directly extended by a servlet, although it's more common to extend
* a protocol-specific subclass such as <code>HttpServlet</code>.
*
* <p><code>GenericServlet</code> makes writing servlets
* easier. It provides simple versions of the lifecycle methods
* <code>init</code> and <code>destroy</code> and of the methods
* in the <code>ServletConfig</code> interface. <code>GenericServlet</code>
* also implements the <code>log</code> method, declared in the
* <code>ServletContext</code> interface.
*
* <p>To write a generic servlet, you need only
* override the abstract <code>service</code> method.
*
* @version $Rev: 46019 $ $Date: 2004-09-14 04:56:06 -0500 (Tue, 14 Sep 2004) $
*/
public abstract class GenericServlet implements Servlet, ServletConfig, java.io.Serializable {
/**
* Does nothing. All of the servlet initialization
* is done by one of the <code>init</code> methods.
*/
public GenericServlet() {
}
/**
* Called by the servlet container to indicate to a servlet that the
* servlet is being taken out of service. See {@link Servlet#destroy}.
*/
public void destroy() {
}
/**
* Returns a <code>String</code> containing the value of the named
* initialization parameter, or <code>null</code> if the parameter does
* not exist. See {@link ServletConfig#getInitParameter}.
*
* <p>This method is supplied for convenience. It gets the
* value of the named parameter from the servlet's
* <code>ServletConfig</code> object.
*
* @param name a <code>String</code> specifying the name
* of the initialization parameter
*
* @return String a <code>String</code> containing the value
* of the initalization parameter
*/
public String getInitParameter(String name) {
return null;
}
/**
* Returns the names of the servlet's initialization parameters
* as an <code>Enumeration</code> of <code>String</code> objects,
* or an empty <code>Enumeration</code> if the servlet has no
* initialization parameters. See {@link
* ServletConfig#getInitParameterNames}.
*
* <p>This method is supplied for convenience. It gets the
* parameter names from the servlet's <code>ServletConfig</code> object.
*
*
* @return Enumeration an enumeration of <code>String</code>
* objects containing the names of the servlet's initialization parameters
*/
public Enumeration getInitParameterNames() {
return null;
}
/**
* Returns this servlet's {@link ServletConfig} object.
*
* @return ServletConfig the <code>ServletConfig</code> object
* that initialized this servlet
*/
public ServletConfig getServletConfig() {
return null;
}
/**
* Returns a reference to the {@link ServletContext} in which this servlet
* is running. See {@link ServletConfig#getServletContext}.
*
* <p>This method is supplied for convenience. It gets the
* context from the servlet's <code>ServletConfig</code> object.
*
*
* @return ServletContext the <code>ServletContext</code> object
* passed to this servlet by the <code>init</code> method
*/
public ServletContext getServletContext() {
return null;
}
/**
* Returns information about the servlet, such as
* author, version, and copyright.
* By default, this method returns an empty string. Override this method
* to have it return a meaningful value. See {@link
* Servlet#getServletInfo}.
*
*
* @return String information about this servlet, by default an
* empty string
*/
public String getServletInfo() {
return null;
}
/**
* Called by the servlet container to indicate to a servlet that the
* servlet is being placed into service. See {@link Servlet#init}.
*
* <p>This implementation stores the {@link ServletConfig}
* object it receives from the servlet container for later use.
* When overriding this form of the method, call
* <code>super.init(config)</code>.
*
* @param config the <code>ServletConfig</code> object
* that contains configutation information for this servlet
*
* @exception ServletException if an exception occurs that
* interrupts the servlet's normal operation
*
* @see UnavailableException
*/
public void init(ServletConfig config) throws ServletException {
}
/**
* A convenience method which can be overridden so that there's no need
* to call <code>super.init(config)</code>.
*
* <p>Instead of overriding {@link #init(ServletConfig)}, simply override
* this method and it will be called by
* <code>GenericServlet.init(ServletConfig config)</code>.
* The <code>ServletConfig</code> object can still be retrieved via {@link
* #getServletConfig}.
*
* @exception ServletException if an exception occurs that
* interrupts the servlet's normal operation
*/
public void init() throws ServletException {
}
/**
* Called by the servlet container to allow the servlet to respond to
* a request. See {@link Servlet#service}.
*
* <p>This method is declared abstract so subclasses, such as
* <code>HttpServlet</code>, must override it.
*
* @param req the <code>ServletRequest</code> object
* that contains the client's request
*
* @param res the <code>ServletResponse</code> object
* that will contain the servlet's response
*
* @exception ServletException if an exception occurs that
* interferes with the servlet's normal operation occurred
*
* @exception IOException if an input or output
* exception occurs
*/
public abstract void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
/**
* Returns the name of this servlet instance.
* See {@link ServletConfig#getServletName}.
*
* @return the name of this servlet instance
*/
public String getServletName() {
return null;
}
}

View File

@@ -24,11 +24,12 @@
package javax.servlet.http;
import java.io.IOException;
import javax.servlet.GenericServlet;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
public abstract class HttpServlet {
public abstract class HttpServlet extends GenericServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
}