diff --git a/CHANGES.txt b/CHANGES.txt index bd58c15343d..8370346c1c9 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1035,6 +1035,10 @@ Trunk (unreleased changes) HADOOP-6257. Two TestFileSystem classes are confusing hadoop-hdfs-hdfwithmr. (Philip Zeyliger via tomwhite) + HADOOP-6151. Added a input filter to all of the http servlets that quotes + html characters in the parameters, to prevent cross site scripting + attacks. (omalley) + Release 0.20.1 - Unreleased INCOMPATIBLE CHANGES diff --git a/src/java/org/apache/hadoop/http/HtmlQuoting.java b/src/java/org/apache/hadoop/http/HtmlQuoting.java new file mode 100644 index 00000000000..a0db635bf17 --- /dev/null +++ b/src/java/org/apache/hadoop/http/HtmlQuoting.java @@ -0,0 +1,198 @@ +/** + * 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.hadoop.http; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * This class is responsible for quoting HTML characters. + */ +public class HtmlQuoting { + private static final byte[] ampBytes = "&".getBytes(); + private static final byte[] aposBytes = "'".getBytes(); + private static final byte[] gtBytes = ">".getBytes(); + private static final byte[] ltBytes = "<".getBytes(); + private static final byte[] quotBytes = """.getBytes(); + + /** + * Does the given string need to be quoted? + * @param data the string to check + * @param off the starting position + * @param len the number of bytes to check + * @return does the string contain any of the active html characters? + */ + public static boolean needsQuoting(byte[] data, int off, int len) { + for(int i=off; i< off+len; ++i) { + switch(data[i]) { + case '&': + case '<': + case '>': + case '\'': + case '"': + return true; + default: + break; + } + } + return false; + } + + /** + * Does the given string need to be quoted? + * @param str the string to check + * @return does the string contain any of the active html characters? + */ + public static boolean needsQuoting(String str) { + byte[] bytes = str.getBytes(); + return needsQuoting(bytes, 0 , bytes.length); + } + + /** + * Quote all of the active HTML characters in the given string as they + * are added to the buffer. + * @param output the stream to write the output to + * @param buffer the byte array to take the characters from + * @param off the index of the first byte to quote + * @param len the number of bytes to quote + */ + public static void quoteHtmlChars(OutputStream output, byte[] buffer, + int off, int len) throws IOException { + for(int i=off; i < off+len; i++) { + switch (buffer[i]) { + case '&': output.write(ampBytes); break; + case '<': output.write(ltBytes); break; + case '>': output.write(gtBytes); break; + case '\'': output.write(aposBytes); break; + case '"': output.write(quotBytes); break; + default: output.write(buffer, i, 1); + } + } + } + + /** + * Quote the given item to make it html-safe. + * @param item the string to quote + * @return the quoted string + */ + public static String quoteHtmlChars(String item) { + byte[] bytes = item.getBytes(); + if (needsQuoting(bytes, 0, bytes.length)) { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + try { + quoteHtmlChars(buffer, bytes, 0, bytes.length); + } catch (IOException ioe) { + // Won't happen, since it is a bytearrayoutputstream + } + return buffer.toString(); + } else { + return item; + } + } + + /** + * Return an output stream that quotes all of the output. + * @param out the stream to write the quoted output to + * @return a new stream that the application show write to + * @throws IOException if the underlying output fails + */ + public static OutputStream quoteOutputStream(final OutputStream out + ) throws IOException { + return new OutputStream() { + private byte[] data = new byte[1]; + @Override + public void write(byte[] data, int off, int len) throws IOException { + quoteHtmlChars(out, data, off, len); + } + + @Override + public void write(int b) throws IOException { + data[0] = (byte) b; + quoteHtmlChars(out, data, 0, 1); + } + + @Override + public void flush() throws IOException { + out.flush(); + } + + @Override + public void close() throws IOException { + out.close(); + } + }; + } + + /** + * Remove HTML quoting from a string. + * @param item the string to unquote + * @return the unquoted string + */ + public static String unquoteHtmlChars(String item) { + int next = item.indexOf('&'); + // nothing was quoted + if (next == -1) { + return item; + } + int len = item.length(); + int posn = 0; + StringBuilder buffer = new StringBuilder(); + while (next != -1) { + buffer.append(item.substring(posn, next)); + if (item.startsWith("&", next)) { + buffer.append('&'); + next += 5; + } else if (item.startsWith("'", next)) { + buffer.append('\''); + next += 6; + } else if (item.startsWith(">", next)) { + buffer.append('>'); + next += 4; + } else if (item.startsWith("<", next)) { + buffer.append('<'); + next += 4; + } else if (item.startsWith(""", next)) { + buffer.append('"'); + next += 6; + } else { + int end = item.indexOf(';', next)+1; + if (end == 0) { + end = len; + } + throw new IllegalArgumentException("Bad HTML quoting for " + + item.substring(next,end)); + } + posn = next; + next = item.indexOf('&', posn); + } + buffer.append(item.substring(posn, len)); + return buffer.toString(); + } + + public static void main(String[] args) throws Exception { + for(String arg:args) { + System.out.println("Original: " + arg); + String quoted = quoteHtmlChars(arg); + System.out.println("Quoted: "+ quoted); + String unquoted = unquoteHtmlChars(quoted); + System.out.println("Unquoted: " + unquoted); + System.out.println(); + } + } +} diff --git a/src/java/org/apache/hadoop/http/HttpServer.java b/src/java/org/apache/hadoop/http/HttpServer.java index 62248e70f1e..800c3e99357 100644 --- a/src/java/org/apache/hadoop/http/HttpServer.java +++ b/src/java/org/apache/hadoop/http/HttpServer.java @@ -23,14 +23,20 @@ import java.net.BindException; import java.net.InetSocketAddress; import java.net.URL; import java.util.ArrayList; +import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.nio.channels.ServerSocketChannel; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; import org.apache.commons.logging.Log; @@ -117,6 +123,7 @@ public class HttpServer implements FilterContainer { addDefaultApps(contexts, appDir); + addGlobalFilter("safety", QuotingInputFilter.class.getName(), null); final FilterInitializer[] initializers = getFilterInitializers(conf); if (initializers != null) { for(FilterInitializer c : initializers) { @@ -512,10 +519,99 @@ public class HttpServer implements FilterContainer { public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - PrintWriter out = new PrintWriter(response.getOutputStream()); + PrintWriter out = new PrintWriter + (HtmlQuoting.quoteOutputStream(response.getOutputStream())); ReflectionUtils.printThreadInfo(out, ""); out.close(); ReflectionUtils.logThreadInfo(LOG, "jsp requested", 1); } } + + /** + * A Servlet input filter that quotes all HTML active characters in the + * parameter names and values. The goal is to quote the characters to make + * all of the servlets resistant to cross-site scripting attacks. + */ + public static class QuotingInputFilter implements Filter { + + public static class RequestQuoter extends HttpServletRequestWrapper { + private final HttpServletRequest rawRequest; + public RequestQuoter(HttpServletRequest rawRequest) { + super(rawRequest); + this.rawRequest = rawRequest; + } + + /** + * Return the set of parameter names, quoting each name. + */ + @SuppressWarnings("unchecked") + @Override + public Enumeration getParameterNames() { + return new Enumeration() { + private Enumeration rawIterator = + rawRequest.getParameterNames(); + @Override + public boolean hasMoreElements() { + return rawIterator.hasMoreElements(); + } + + @Override + public String nextElement() { + return HtmlQuoting.quoteHtmlChars(rawIterator.nextElement()); + } + }; + } + + /** + * Unquote the name and quote the value. + */ + @Override + public String getParameter(String name) { + return HtmlQuoting.quoteHtmlChars(rawRequest.getParameter + (HtmlQuoting.unquoteHtmlChars(name))); + } + + @Override + public String[] getParameterValues(String name) { + String unquoteName = HtmlQuoting.unquoteHtmlChars(name); + String[] unquoteValue = rawRequest.getParameterValues(unquoteName); + String[] result = new String[unquoteValue.length]; + for(int i=0; i < result.length; ++i) { + result[i] = HtmlQuoting.quoteHtmlChars(unquoteValue[i]); + } + return result; + } + + @SuppressWarnings("unchecked") + @Override + public Map getParameterMap() { + Map result = new HashMap(); + Map raw = rawRequest.getParameterMap(); + for (Map.Entry item: raw.entrySet()) { + result.put(HtmlQuoting.quoteHtmlChars(item.getKey()), + HtmlQuoting.quoteHtmlChars(item.getValue())); + } + return result; + } + } + + @Override + public void init(FilterConfig config) throws ServletException { + } + + @Override + public void destroy() { + } + + @Override + public void doFilter(ServletRequest request, + ServletResponse response, + FilterChain chain + ) throws IOException, ServletException { + HttpServletRequestWrapper quoted = + new RequestQuoter((HttpServletRequest) request); + chain.doFilter(quoted, response); + } + + } } diff --git a/src/test/core/org/apache/hadoop/http/TestHtmlQuoting.java b/src/test/core/org/apache/hadoop/http/TestHtmlQuoting.java new file mode 100644 index 00000000000..f6abf672081 --- /dev/null +++ b/src/test/core/org/apache/hadoop/http/TestHtmlQuoting.java @@ -0,0 +1,62 @@ +/** + * 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.hadoop.http; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class TestHtmlQuoting { + + @Test public void testNeedsQuoting() throws Exception { + assertTrue(HtmlQuoting.needsQuoting("abcde>")); + assertTrue(HtmlQuoting.needsQuoting("")); + assertEquals("&&&", HtmlQuoting.quoteHtmlChars("&&&")); + assertEquals(" '\n", HtmlQuoting.quoteHtmlChars(" '\n")); + assertEquals(""", HtmlQuoting.quoteHtmlChars("\"")); + } + + private void runRoundTrip(String str) throws Exception { + assertEquals(str, + HtmlQuoting.unquoteHtmlChars(HtmlQuoting.quoteHtmlChars(str))); + } + + @Test public void testRoundtrip() throws Exception { + runRoundTrip(""); + runRoundTrip("<>&'\""); + runRoundTrip("ab>cd