Author: omalley
Date: Fri Sep 18 16:31:34 2009
New Revision: 816703
URL: http://svn.apache.org/viewvc?rev=816703&view=rev
Log:
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)
Added:
hadoop/common/trunk/src/java/org/apache/hadoop/http/HtmlQuoting.java
hadoop/common/trunk/src/test/core/org/apache/hadoop/http/TestHtmlQuoting.java
Modified:
hadoop/common/trunk/CHANGES.txt
hadoop/common/trunk/src/java/org/apache/hadoop/http/HttpServer.java
Modified: hadoop/common/trunk/CHANGES.txt
URL:
http://svn.apache.org/viewvc/hadoop/common/trunk/CHANGES.txt?rev=816703&r1=816702&r2=816703&view=diff
==============================================================================
--- hadoop/common/trunk/CHANGES.txt (original)
+++ hadoop/common/trunk/CHANGES.txt Fri Sep 18 16:31:34 2009
@@ -1035,6 +1035,10 @@
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
Added: hadoop/common/trunk/src/java/org/apache/hadoop/http/HtmlQuoting.java
URL:
http://svn.apache.org/viewvc/hadoop/common/trunk/src/java/org/apache/hadoop/http/HtmlQuoting.java?rev=816703&view=auto
==============================================================================
--- hadoop/common/trunk/src/java/org/apache/hadoop/http/HtmlQuoting.java (added)
+++ hadoop/common/trunk/src/java/org/apache/hadoop/http/HtmlQuoting.java Fri
Sep 18 16:31:34 2009
@@ -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();
+ }
+ }
+}
Modified: hadoop/common/trunk/src/java/org/apache/hadoop/http/HttpServer.java
URL:
http://svn.apache.org/viewvc/hadoop/common/trunk/src/java/org/apache/hadoop/http/HttpServer.java?rev=816703&r1=816702&r2=816703&view=diff
==============================================================================
--- hadoop/common/trunk/src/java/org/apache/hadoop/http/HttpServer.java
(original)
+++ hadoop/common/trunk/src/java/org/apache/hadoop/http/HttpServer.java Fri Sep
18 16:31:34 2009
@@ -23,14 +23,20 @@
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 @@
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 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<String> getParameterNames() {
+ return new Enumeration<String>() {
+ private Enumeration<String> 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<String, String> getParameterMap() {
+ Map<String, String> result = new HashMap<String,String>();
+ Map<String, String> raw = rawRequest.getParameterMap();
+ for (Map.Entry<String,String> 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);
+ }
+
+ }
}
Added:
hadoop/common/trunk/src/test/core/org/apache/hadoop/http/TestHtmlQuoting.java
URL:
http://svn.apache.org/viewvc/hadoop/common/trunk/src/test/core/org/apache/hadoop/http/TestHtmlQuoting.java?rev=816703&view=auto
==============================================================================
---
hadoop/common/trunk/src/test/core/org/apache/hadoop/http/TestHtmlQuoting.java
(added)
+++
hadoop/common/trunk/src/test/core/org/apache/hadoop/http/TestHtmlQuoting.java
Fri Sep 18 16:31:34 2009
@@ -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("<abcde"));
+ assertTrue(HtmlQuoting.needsQuoting("abc'de"));
+ assertTrue(HtmlQuoting.needsQuoting("abcde\""));
+ assertTrue(HtmlQuoting.needsQuoting("&"));
+ assertFalse(HtmlQuoting.needsQuoting(""));
+ assertFalse(HtmlQuoting.needsQuoting("ab\ncdef"));
+ }
+
+ @Test public void testQuoting() throws Exception {
+ assertEquals("ab<cd", HtmlQuoting.quoteHtmlChars("ab<cd"));
+ assertEquals("ab>", HtmlQuoting.quoteHtmlChars("ab>"));
+ 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<ef&ghi'\"");
+ runRoundTrip("A string\n with no quotable chars in it!");
+ StringBuilder buffer = new StringBuilder();
+ for(char ch=0; ch < 127; ++ch) {
+ buffer.append(ch);
+ }
+ runRoundTrip(buffer.toString());
+ }
+}