/******************************************************************************* * 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.wink.server.internal.providers.entity; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.InputStreamReader; import java.io.StringReader; import java.io.StringWriter; import java.util.StringTokenizer; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.core.MediaType; import javax.ws.rs.ext.MessageBodyReader; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.transform.Source; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.TransformerFactoryConfigurationError; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.sax.SAXSource; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import org.apache.wink.common.RuntimeContext; import org.apache.wink.common.internal.WinkConfiguration; import org.apache.wink.common.internal.providers.entity.SourceProvider; import org.apache.wink.common.internal.runtime.RuntimeContextTLS; import org.apache.wink.server.internal.servlet.MockServletInvocationTest; import org.apache.wink.test.mock.MockRequestConstructor; import org.jmock.Expectations; import org.jmock.Mockery; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.w3c.dom.Document; import org.xml.sax.InputSource; import org.xml.sax.SAXParseException; import org.xml.sax.XMLReader; import org.xml.sax.helpers.XMLReaderFactory; public class SourceProviderTest extends MockServletInvocationTest { private static final String SOURCE = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><message>this is a test message</message>"; private static final byte[] SOURCE_BYTES = SOURCE.getBytes(); private String TEST_CLASSES_PATH = null; @Override public void setUp() throws Exception { super.setUp(); Mockery mockery = new Mockery(); final RuntimeContext context = mockery.mock(RuntimeContext.class); mockery.checking(new Expectations() {{ allowing(context).getAttribute(WinkConfiguration.class); will(returnValue(null)); }}); RuntimeContextTLS.setRuntimeContext(context); } @Override public void tearDown() { RuntimeContextTLS.setRuntimeContext(null); } private String getPath() { if (TEST_CLASSES_PATH == null) { String classpath = System.getProperty("java.class.path"); StringTokenizer tokenizer = new StringTokenizer(classpath, System.getProperty("path.separator")); TEST_CLASSES_PATH = null; while (tokenizer.hasMoreTokens()) { TEST_CLASSES_PATH = tokenizer.nextToken(); if (TEST_CLASSES_PATH.endsWith("test-classes")) { break; } } // for windows: int driveIndex = TEST_CLASSES_PATH.indexOf(":"); if(driveIndex != -1) { TEST_CLASSES_PATH = TEST_CLASSES_PATH.substring(driveIndex + 1); } } return TEST_CLASSES_PATH; } @Override protected Class<?>[] getClasses() { return new Class<?>[] {SourceResource.class}; } @Path("source") public static class SourceResource { private static final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory .newInstance(); private static final TransformerFactory transformerFactory = TransformerFactory .newInstance(); @GET @Path("stream") public Source getStream() { return new StreamSource(new ByteArrayInputStream(SOURCE_BYTES)); } @GET @Path("sax") public Source getSax() { return new SAXSource(new InputSource(new ByteArrayInputStream(SOURCE_BYTES))); } @GET @Path("dom") public Source getDom() throws Exception { return new DOMSource(documentBuilderFactory.newDocumentBuilder() .parse(new ByteArrayInputStream(SOURCE_BYTES))); } @POST @Path("stream") public String postStream(StreamSource source) throws Exception { return extractXml(source); } @POST @Path("sax") public String postSax(SAXSource source) throws Exception { return extractXml(source); } @POST @Path("saxwithdtd") public String postSaxWithDTD(SAXSource source) throws Exception { /* * we don't want to trigger a parse in this resource method. We're testing to see what happened * with the SAXSource on the way here. */ StringBuilder sb = new StringBuilder(); String line; InputStream is = source.getInputSource().getByteStream(); try { BufferedReader reader = new BufferedReader(new InputStreamReader(is, "UTF-8")); while ((line = reader.readLine()) != null) { sb.append(line).append("\n"); } } finally { is.close(); } return sb.toString(); } @POST @Path("dom") public String postDom(DOMSource source) throws Exception { return extractXml(source); } @POST @Path("domwithdtd") public String postDomWithDTD(DOMSource source) throws Exception { /* * we don't want to trigger a parse in this resource method. We're testing to see what happened * with the SAXSource on the way here. */ return source.getNode().getFirstChild().getFirstChild().getTextContent(); } private String extractXml(Source source) throws TransformerFactoryConfigurationError, TransformerConfigurationException, TransformerException { Transformer transformer = transformerFactory.newTransformer(); StringWriter sw = new StringWriter(); StreamResult sr = new StreamResult(sw); transformer.transform(source, sr); return sw.toString(); } } public void testSourceProvider() throws Exception { // stream source SourceProvider provider = new SourceProvider.StreamSourceProvider(); Source source = assertSourceReader(provider, StreamSource.class); assertSourceWriter(provider, source); // sax source provider = new SourceProvider.SAXSourceProvider(); source = assertSourceReader(provider, SAXSource.class); assertSourceWriter(provider, source); // dom source provider = new SourceProvider.DOMSourceProvider(); source = assertSourceReader(provider, DOMSource.class); assertSourceWriter(provider, source); } public void testSourceProviderInvocation() throws Exception { MockHttpServletRequest request = MockRequestConstructor.constructMockRequest("GET", "/source/stream", "text/xml"); MockHttpServletResponse response = invoke(request); assertEquals(200, response.getStatus()); assertEquals(SOURCE, response.getContentAsString()); request = MockRequestConstructor.constructMockRequest("GET", "/source/sax", "text/xml"); response = invoke(request); assertEquals(200, response.getStatus()); assertEquals(SOURCE, response.getContentAsString()); request = MockRequestConstructor.constructMockRequest("GET", "/source/dom", "text/xml"); response = invoke(request); assertEquals(200, response.getStatus()); // Ignore Xml declaration, since in 1.6 Transformer always generates xml // with "standalone" attribute // when DOMSource is serialized assertEqualsEgnoreXmlDecl(SOURCE, response.getContentAsString()); request = MockRequestConstructor.constructMockRequest("POST", "/source/stream", "text/xml", "text/xml", SOURCE_BYTES); response = invoke(request); assertEquals(200, response.getStatus()); assertEquals(SOURCE, response.getContentAsString()); request = MockRequestConstructor.constructMockRequest("POST", "/source/sax", "text/xml", "text/xml", SOURCE_BYTES); response = invoke(request); assertEquals(200, response.getStatus()); assertEquals(SOURCE, response.getContentAsString()); request = MockRequestConstructor.constructMockRequest("POST", "/source/dom", "text/xml", "text/xml", SOURCE_BYTES); response = invoke(request); assertEquals(200, response.getStatus()); // Ignore Xml declaration, since in 1.6 Transformer always generates xml // with "standalone" attribute // when DOMSource is serialized assertEqualsEgnoreXmlDecl(SOURCE, response.getContentAsString()); } public void testSaxWithDTD() throws Exception { String path = getPath(); final String SOURCE = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + "<!DOCTYPE data [<!ENTITY file SYSTEM \""+ path +"/etc/SourceProviderTest.txt\">]>" + "<message>&file;</message>"; final byte[] SOURCE_BYTES = SOURCE.getBytes(); MockHttpServletRequest request = MockRequestConstructor.constructMockRequest("POST", "/source/saxwithdtd", "application/xml", "application/xml", SOURCE_BYTES); MockHttpServletResponse response = invoke(request); assertEquals(200, response.getStatus()); assertFalse("File content is visible but should not be.", response.getContentAsString().contains("YOU SHOULD NOT BE ABLE TO SEE THIS")); assertEquals(SOURCE, response.getContentAsString().trim()); } public void testDomWithDTD() throws Exception { String path = getPath(); final String SOURCE = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + "<!DOCTYPE data [<!ENTITY file SYSTEM \""+ path +"/etc/SourceProviderTest.txt\">]>" + "<message>&file;</message>"; final byte[] SOURCE_BYTES = SOURCE.getBytes(); MockHttpServletRequest request = MockRequestConstructor.constructMockRequest("POST", "/source/domwithdtd", "application/xml", "application/xml", SOURCE_BYTES); MockHttpServletResponse response = invoke(request); assertEquals(400, response.getStatus()); // as a sanity check, let's make sure our xml is good: DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); InputSource is = new InputSource( new StringReader(SOURCE) ); Document d = builder.parse( is ); assertEquals("xml is bad", "YOU SHOULD NOT BE ABLE TO SEE THIS", d.getElementsByTagName("message").item(0).getTextContent().trim()); } public void testDomWithDTDEntityExpansionAttack1() throws Exception { String classpath = System.getProperty("java.class.path"); StringTokenizer tokenizer = new StringTokenizer(classpath, System.getProperty("path.separator")); String path = null; while (tokenizer.hasMoreTokens()) { path = tokenizer.nextToken(); if (path.endsWith("test-classes")) { break; } } final String SOURCE = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>" + "<!DOCTYPE root [" + "<!ENTITY % a \"x\">" + "<!ENTITY % b \"%a;%a;\">" + "]>" + "<message>&b;</message>"; final byte[] SOURCE_BYTES = SOURCE.getBytes(); MockHttpServletRequest request = MockRequestConstructor.constructMockRequest("POST", "/source/domwithdtd", "application/xml", "application/xml", SOURCE_BYTES); // The SAX parser will not allow entity refs inside a DTD. There is no special Wink code necessary for this. MockHttpServletResponse response = invoke(request); assertEquals(400, response.getStatus()); // as a sanity check, let's make sure the xml is good: XMLReader xmlReader = XMLReaderFactory.createXMLReader(); try { xmlReader.parse(new InputSource(new ByteArrayInputStream(SOURCE_BYTES))); fail("expected SAXParseException"); } catch (SAXParseException e) { } } public void testDomWithDTDEntityExpansionAttack2() throws Exception { String classpath = System.getProperty("java.class.path"); StringTokenizer tokenizer = new StringTokenizer(classpath, System.getProperty("path.separator")); String path = null; while (tokenizer.hasMoreTokens()) { path = tokenizer.nextToken(); if (path.endsWith("test-classes")) { break; } } final String SOURCE = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>" + "<!DOCTYPE root [" + "<!ENTITY x32 \"foobar\">" + "<!ENTITY x31 \"&x32;&x32;\">" + "<!ENTITY x30 \"&x31;&x31;\">" + "<!ENTITY x29 \"&x30;&x30;\">" + "<!ENTITY x28 \"&x29;&x29;\">" + "<!ENTITY x27 \"&x28;&x28;\">" + "<!ENTITY x26 \"&x27;&x27;\">" + "<!ENTITY x25 \"&x26;&x26;\">" + "<!ENTITY x24 \"&x25;&x25;\">" + "<!ENTITY x23 \"&x24;&x24;\">" + "<!ENTITY x22 \"&x23;&x23;\">" + "<!ENTITY x21 \"&x22;&x22;\">" + "<!ENTITY x20 \"&x21;&x21;\">" + "<!ENTITY x19 \"&x20;&x20;\">" + "<!ENTITY x18 \"&x19;&x19;\">" + "<!ENTITY x17 \"&x18;&x18;\">" + "<!ENTITY x16 \"&x17;&x17;\">" + "<!ENTITY x15 \"&x16;&x16;\">" + "<!ENTITY x14 \"&x15;&x15;\">" + "<!ENTITY x13 \"&x14;&x14;\">" + "<!ENTITY x12 \"&x13;&x13;\">" + "<!ENTITY x11 \"&x12;&x12;\">" + "<!ENTITY x10 \"&x11;&x11;\">" + "<!ENTITY x9 \"&x10;&x10;\">" + "<!ENTITY x8 \"&x9;&x9;\">" + "<!ENTITY x7 \"&x8;&x8;\">" + "<!ENTITY x6 \"&x7;&x7;\">" + "<!ENTITY x5 \"&x6;&x6;\">" + "<!ENTITY x4 \"&x5;&x5;\">" + "<!ENTITY x3 \"&x4;&x4;\">" + "<!ENTITY x2 \"&x3;&x3;\">" + "<!ENTITY x1 \"&x2;&x2;\">" + "]>" + "<message>&x1;</message>"; final byte[] SOURCE_BYTES = SOURCE.getBytes(); MockHttpServletRequest request = MockRequestConstructor.constructMockRequest("POST", "/source/domwithdtd", "application/xml", "application/xml", SOURCE_BYTES); MockHttpServletResponse response = invoke(request); assertEquals(400, response.getStatus()); } // -- Helpers private void assertSourceWriter(SourceProvider provider, Source source) throws Exception { assertTrue(provider .isWriteable(source.getClass(), null, null, new MediaType("text", "xml"))); assertTrue(provider.isWriteable(source.getClass(), null, null, new MediaType("application", "xml"))); assertTrue(provider.isWriteable(source.getClass(), null, null, new MediaType("application", "atom+xml"))); assertTrue(provider .isWriteable(source.getClass(), null, null, new MediaType("application", "atomsvc+xml"))); assertFalse(provider.isWriteable(source.getClass(), null, null, new MediaType("text", "plain"))); ByteArrayOutputStream os = new ByteArrayOutputStream(); provider.writeTo(source, null, null, null, new MediaType("text", "xml"), null, os); // Ignore Xml declaration, since in 1.6 Transformer always generates xml // with "standalone" attribute // when DOMSource is serialized assertEqualsEgnoreXmlDecl(SOURCE, os.toString()); } @SuppressWarnings("unchecked") private Source assertSourceReader(SourceProvider provider, Class<?> sourceClass) throws Exception { ByteArrayInputStream inputStream = new ByteArrayInputStream(SOURCE.getBytes("UTF-8")); MessageBodyReader<Source> bodyReader = (MessageBodyReader<Source>)provider; assertTrue(bodyReader.isReadable(sourceClass, null, null, new MediaType("text", "xml"))); assertTrue(bodyReader.isReadable(sourceClass, null, null, new MediaType("application", "xml"))); assertTrue(bodyReader.isReadable(sourceClass, null, null, new MediaType("application", "atom+xml"))); assertTrue(bodyReader.isReadable(sourceClass, null, null, new MediaType("application", "atomsvc+xml"))); assertFalse(bodyReader.isReadable(sourceClass, null, null, new MediaType("text", "plain"))); Source source = bodyReader.readFrom((Class<Source>)sourceClass, null, null, new MediaType("text", "xml"), null, inputStream); assertNotNull(source); assertEquals(sourceClass, source.getClass()); return source; } private void assertEqualsEgnoreXmlDecl(String expected, String actual) { expected = removeXmlDecl(expected); actual = removeXmlDecl(actual); assertEquals(expected, actual); } private String removeXmlDecl(String expected) { if (expected.indexOf("<?xml") >= 0) { expected = expected.substring(expected.indexOf("?>")); } return expected; } }