For more information on deserialization checkout the OWASP * Cheat Sheet. */ -@IgnoreJRERequirement public final class ObjectInputFilters { private ObjectInputFilters() {} @@ -80,7 +76,6 @@ public static ObjectInputFilter createCombinedHardenedObjectFilter( return new CombinedObjectInputFilter(existingFilter); } - @IgnoreJRERequirement private static class CombinedObjectInputFilter implements ObjectInputFilter { private final ObjectInputFilter originalFilter; @@ -97,30 +92,6 @@ public Status checkInput(final FilterInfo filterInfo) { } } - /** - * This method returns a wrapped {@link ObjectInputStream} that protects against deserialization - * code execution attacks. This method can be used in Java 8 and previous. - * - * @param ois the stream to wrap and harden - * @return an {@link ObjectInputStream} which is safe against all publicly known gadgets - * @throws IOException if the underlying creation of {@link ObjectInputStream} fails - */ - public static ObjectInputStream createSafeObjectInputStream(final InputStream ois) - throws IOException { - try { - final ValidatingObjectInputStream is = new ValidatingObjectInputStream(ois); - for (String gadget : UnwantedTypes.dangerousClassNameTokens()) { - is.reject("*" + gadget + "*"); - } - return is; - } catch (IOException e) { - // ignored - } - - // if for some reason we can't replace it, we'll pass it back as it was given - return new ObjectInputStream(ois); - } - private static final ObjectInputFilter basicGadgetDenylistFilter = ObjectInputFilter.Config.createFilter( "!" + String.join("*;!", UnwantedTypes.dangerousClassNameTokens())); diff --git a/src/test/java/io/github/pixee/security/ObjectInputFiltersTest.java b/src/java11Test/java/io/github/pixee/security/ObjectInputFiltersTest.java similarity index 94% rename from src/test/java/io/github/pixee/security/ObjectInputFiltersTest.java rename to src/java11Test/java/io/github/pixee/security/ObjectInputFiltersTest.java index d4c64b5..ca79d04 100644 --- a/src/test/java/io/github/pixee/security/ObjectInputFiltersTest.java +++ b/src/java11Test/java/io/github/pixee/security/ObjectInputFiltersTest.java @@ -41,17 +41,6 @@ void default_is_unprotected() throws Exception { assertThat(o, instanceOf(DiskFileItem.class)); } - @Test - void validating_ois_works() throws Exception { - ObjectInputStream ois = - ObjectInputFilters.createSafeObjectInputStream(new ByteArrayInputStream(serializedGadget)); - assertThrows( - InvalidClassException.class, - () -> { - ois.readObject(); - fail("this should have been blocked"); - }); - } @Test void ois_harden_works() throws Exception { diff --git a/src/main/java/io/github/pixee/security/HtmlEncoder.java b/src/main/java/io/github/pixee/security/HtmlEncoder.java index 6e5e072..0a71f18 100644 --- a/src/main/java/io/github/pixee/security/HtmlEncoder.java +++ b/src/main/java/io/github/pixee/security/HtmlEncoder.java @@ -1,7 +1,5 @@ package io.github.pixee.security; -import com.coverity.security.Escape; - /** * This type exposes helper methods that will help defend against XSS attacks with HTML encoding. * @@ -21,4 +19,292 @@ private HtmlEncoder() {} public static String encode(final String s) { return Escape.html(s); } + + /* + * This code was originally brought in as the BSD-2 licensed dependency from Coverity. However, it didn't publish any automatic module name, and we didn't really need any of the rest of it, so we just copied the code here, including the license. + */ + + /** + * Copyright (c) 2012-2016, Coverity, Inc. All rights reserved. + * + *
Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: - Redistributions of source code must + * retain the above copyright notice, this list of conditions and the following disclaimer. - + * Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided + * with the distribution. - Neither the name of Coverity, Inc. nor the names of its contributors + * may be used to endorse or promote products derived from this software without specific prior + * written permission from Coverity, Inc. + * + *
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND INFRINGEMENT ARE DISCLAIMED. IN NO EVENT + * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + /** + * Escape is a small set of methods for escaping tainted data. These escaping methods are useful + * in transforming user-controlled ("tainted") data into forms that are safe from being + * interpreted as something other than data, such as JavaScript. + * + *
At this time most of these escaping routines focus on cross-site scripting mitigations. Each + * method is good for a different HTML context. For a primer on HTML contexts, see OWASP's XSS + * Prevention Cheat Sheet (note however that the escaping routines are not implemented exactly + * according to OWASP's recommendations) or the Coverity Security Advisor documentation. Also see + * the Coverity Security Research Laboratory blog on how to properly use each function. + * + *
While Coverity's static analysis product references these escaping routines as exemplars and + * understands their behavior, there is no dependency on Coverity products and these routines are + * completely standalone. Feel free to use them! Just make sure you use them correctly. + * + * @author Romain Gaucher + * @author Andy Chou + * @author Jon Passki + * @author Alex Kouzemtchenko + */ + private static class Escape { + + /** + * HTML entity escaping for text content and attributes. + * + *
HTML entity escaping that is appropriate for the most common HTML contexts: PCDATA and
+ * "normal" attributes (non-URI, non-event, and non-CSS attributes).
+ * Note that we do not recommend using non-quoted HTML attributes since the security obligations
+ * vary more between web browser. We recommend to always quote (single or double quotes) HTML
+ * attributes.
+ * This method is generic to HTML entity escaping, and therefore escapes more characters than
+ * usually necessary -- mostly to handle non-quoted attribute values. If this method is somehow
+ * too slow, such as you output megabytes of text with spaces, please use the {@link
+ * #htmlText(String)} method which only escape HTML text specific characters.
+ *
+ *
The following characters are escaped: + * + *
' (U+0022), " (U+0027), \ (U+005C)
+ * , / (U+002F), < (U+003C), > (U+003E)
+ * , & (U+0026)
+ * \t (U+0009), \n (U+000A),
+ * \f (U+000C), \r (U+000D), SPACE (U+0020)
+ * LS (U+2028), PS (U+2029)
+ * null if input is null
+ * @since 1.0
+ */
+ private static String html(String input) {
+ if (input == null) return null;
+
+ int length = input.length();
+ StringBuilder output = allocateStringBuilder(length);
+
+ for (int i = 0; i < length; i++) {
+ char c = input.charAt(i);
+ switch (c) {
+ // Control chars
+ case '\t':
+ output.append(" ");
+ break;
+ case '\n':
+ output.append("
");
+ break;
+ case '\f':
+ output.append("");
+ break;
+ case '\r':
+ output.append("
");
+ break;
+ // Chars that have a meaning for HTML
+ case '\'':
+ output.append("'");
+ break;
+ case '\\':
+ output.append("\");
+ break;
+ case ' ':
+ output.append(" ");
+ break;
+ case '/':
+ output.append("/");
+ break;
+ case '"':
+ output.append(""");
+ break;
+ case '<':
+ output.append("<");
+ break;
+ case '>':
+ output.append(">");
+ break;
+ case '&':
+ output.append("&");
+ break;
+ // Unicode new lines
+ case '\u2028':
+ output.append("
");
+ break;
+ case '\u2029':
+ output.append("
");
+ break;
+
+ default:
+ output.append(c);
+ break;
+ }
+ }
+ return output.toString();
+ }
+
+ /**
+ * URI encoder.
+ *
+ * URI encoding for query string values of the URI:
+ * /example/?name=URI_ENCODED_VALUE_HERE
+ * Note that this method is not sufficient to protect for cross-site scripting in a generic URI
+ * context, but only for query string values. If you need to escape a URI in an href
+ * attribute (for example), ensure that:
+ *
+ *
' (U+0022), " (U+0027), \ (U+005C)
+ * , / (U+002F), < (U+003C), > (U+003E)
+ * , & (U+0026), < (U+003C), > (U+003E)
+ * , ! (U+0021), # (U+0023), $ (U+0024),
+ * % (U+0025), ( (U+0028), ) (U+0029),
+ * * (U+002A), + (U+002B), , (U+002C), . (U+002E)
+ * , : (U+003A), ; (U+003B), = (U+003D),
+ * ? (U+003F), @ (U+0040), [ (U+005B),
+ * ] (U+005D)
+ * \t (U+0009), \n (U+000A),
+ * \f (U+000C), \r (U+000D), SPACE (U+0020)
+ * null if input is null
+ * @since 1.0
+ */
+ private static String uriParam(String input) {
+ if (input == null) return null;
+
+ int length = input.length();
+ StringBuilder output = allocateStringBuilder(length);
+
+ for (int i = 0; i < length; i++) {
+ char c = input.charAt(i);
+ switch (c) {
+ // Control chars
+ case '\t':
+ output.append("%09");
+ break;
+ case '\n':
+ output.append("%0A");
+ break;
+ case '\f':
+ output.append("%0C");
+ break;
+ case '\r':
+ output.append("%0D");
+ break;
+ // RFC chars to encode, plus % ' " < and >, and space
+ case ' ':
+ output.append("%20");
+ break;
+ case '!':
+ output.append("%21");
+ break;
+ case '"':
+ output.append("%22");
+ break;
+ case '#':
+ output.append("%23");
+ break;
+ case '$':
+ output.append("%24");
+ break;
+ case '%':
+ output.append("%25");
+ break;
+ case '&':
+ output.append("%26");
+ break;
+ case '\'':
+ output.append("%27");
+ break;
+ case '(':
+ output.append("%28");
+ break;
+ case ')':
+ output.append("%29");
+ break;
+ case '*':
+ output.append("%2A");
+ break;
+ case '+':
+ output.append("%2B");
+ break;
+ case ',':
+ output.append("%2C");
+ break;
+ case '.':
+ output.append("%2E");
+ break;
+ case '/':
+ output.append("%2F");
+ break;
+ case ':':
+ output.append("%3A");
+ break;
+ case ';':
+ output.append("%3B");
+ break;
+ case '<':
+ output.append("%3C");
+ break;
+ case '=':
+ output.append("%3D");
+ break;
+ case '>':
+ output.append("%3E");
+ break;
+ case '?':
+ output.append("%3F");
+ break;
+ case '@':
+ output.append("%40");
+ break;
+ case '[':
+ output.append("%5B");
+ break;
+ case ']':
+ output.append("%5D");
+ break;
+
+ default:
+ output.append(c);
+ break;
+ }
+ }
+ return output.toString();
+ }
+
+ /** Compute the allocation size of the StringBuilder based on the length. */
+ private static StringBuilder allocateStringBuilder(int length) {
+ // Allocate enough temporary buffer space to avoid reallocation in most
+ // cases. If you believe you will output large amount of data at once
+ // you might need to change the factor.
+ int buflen = length;
+ if (length * 2 > 0) buflen = length * 2;
+ return new StringBuilder(buflen);
+ }
+ }
}
diff --git a/src/main/java/io/github/pixee/security/ObjectInputStreams.java b/src/main/java/io/github/pixee/security/ObjectInputStreams.java
new file mode 100644
index 0000000..4fc120f
--- /dev/null
+++ b/src/main/java/io/github/pixee/security/ObjectInputStreams.java
@@ -0,0 +1,40 @@
+package io.github.pixee.security;
+
+import org.apache.commons.io.serialization.ValidatingObjectInputStream;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+
+/**
+ * This type exposes helper methods that will help defend against Java deserialization attacks
+ * leveraging {@link ObjectInputStream} APIs.
+ *
+ * For more information on deserialization checkout the OWASP + * Cheat Sheet. + */ +public final class ObjectInputStreams { + + /** + * Private no-op constructor to prevent accidental initialization of this class + */ + private ObjectInputStreams() {} + + /** + * This method returns a wrapped {@link ObjectInputStream} that protects against deserialization + * code execution attacks. This method can be used in Java 8 and previous. + * + * @param ois the stream to wrap and harden + * @return an {@link ObjectInputStream} which is safe against all publicly known gadgets + * @throws IOException if the underlying creation of {@link ObjectInputStream} fails + */ + public static ObjectInputStream createValidatingObjectInputStream(final InputStream ois) + throws IOException { + final ValidatingObjectInputStream is = new ValidatingObjectInputStream(ois); + for (String gadget : UnwantedTypes.dangerousClassNameTokens()) { + is.reject("*" + gadget + "*"); + } + return is; + } +} diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java new file mode 100644 index 0000000..8384d9e --- /dev/null +++ b/src/main/java/module-info.java @@ -0,0 +1,9 @@ +open module io.github.pixee.security { + exports io.github.pixee.security; + exports io.github.pixee.security.jakarta; + + requires org.apache.commons.io; + requires java.xml; + requires java.desktop; + requires java.base; +} \ No newline at end of file diff --git a/src/test/java/io/github/pixee/security/ObjectInputStreamsTest.java b/src/test/java/io/github/pixee/security/ObjectInputStreamsTest.java new file mode 100644 index 0000000..24a1d84 --- /dev/null +++ b/src/test/java/io/github/pixee/security/ObjectInputStreamsTest.java @@ -0,0 +1,54 @@ +package io.github.pixee.security; + +import org.apache.commons.fileupload.disk.DiskFileItem; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InvalidClassException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.nio.file.Files; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.fail; + +final class ObjectInputStreamsTest { + + private static DiskFileItem gadget; // this is an evil gadget type + private static byte[] serializedGadget; // this the serialized bytes of that gadget + + @BeforeAll + static void setup() throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + gadget = + new DiskFileItem( + "fieldName", + "text/html", + false, + "foo.html", + 100, + Files.createTempDirectory("adi").toFile()); + gadget.getOutputStream(); // needed to make the object serializable + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(gadget); + serializedGadget = baos.toByteArray(); + } + + + @Test + void validating_ois_works() throws Exception { + ObjectInputStream ois = + ObjectInputStreams.createValidatingObjectInputStream(new ByteArrayInputStream(serializedGadget)); + assertThrows( + InvalidClassException.class, + () -> { + ois.readObject(); + fail("this should have been blocked"); + }); + } + + +} \ No newline at end of file diff --git a/test-apps/hello-world-modules/build.gradle.kts b/test-apps/hello-world-modules/build.gradle.kts new file mode 100644 index 0000000..e3993e5 --- /dev/null +++ b/test-apps/hello-world-modules/build.gradle.kts @@ -0,0 +1,27 @@ +plugins { + java + id("com.google.cloud.tools.jib") version "3.4.0" +} + +repositories { + mavenCentral() +} + + +java { + modularity.inferModulePath.set(true) + toolchain { + languageVersion.set(JavaLanguageVersion.of(11)) + } +} + +jib.container { + entrypoint = listOf("java", "--module-path", "@/app/jib-classpath-file", "-m", "io.github.pixee.testapp/io.github.pixee.testapp.Main") +} + +jib.to.image = "pixee/${project.name}" + + +dependencies { + implementation(project.rootProject) +} \ No newline at end of file diff --git a/test-apps/hello-world-modules/src/main/java/io/github/pixee/testapp/Main.java b/test-apps/hello-world-modules/src/main/java/io/github/pixee/testapp/Main.java new file mode 100644 index 0000000..9b89062 --- /dev/null +++ b/test-apps/hello-world-modules/src/main/java/io/github/pixee/testapp/Main.java @@ -0,0 +1,15 @@ +package io.github.pixee.testapp; + +import io.github.pixee.security.HostValidator; + +public final class Main { +/** A simple piece of code that references the library, so we know the module visibility is correct. */ + public static void main(final String[] args) { + String message = "Hello, World!"; + if (HostValidator.DENY_COMMON_INFRASTRUCTURE_TARGETS.isAllowed(message)) { + System.out.println(message); + } else { + System.out.println("Access denied"); + } + } +} diff --git a/test-apps/hello-world-modules/src/main/java/module-info.java b/test-apps/hello-world-modules/src/main/java/module-info.java new file mode 100644 index 0000000..d69535a --- /dev/null +++ b/test-apps/hello-world-modules/src/main/java/module-info.java @@ -0,0 +1,5 @@ +module io.github.pixee.testapp { + exports io.github.pixee.testapp; + + requires io.github.pixee.security; +} \ No newline at end of file diff --git a/test-apps/hello-world/build.gradle.kts b/test-apps/hello-world/build.gradle.kts new file mode 100644 index 0000000..2e085a7 --- /dev/null +++ b/test-apps/hello-world/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + java + id("com.google.cloud.tools.jib") version "3.4.0" +} + +repositories { + mavenCentral() +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(8)) + } +} + +jib.to.image = "pixee/${project.name}" + +dependencies { + compileOnly(project.rootProject) +} \ No newline at end of file diff --git a/test-apps/hello-world/src/main/java/io/github/pixee/testapp/Main.java b/test-apps/hello-world/src/main/java/io/github/pixee/testapp/Main.java new file mode 100644 index 0000000..15b0a4e --- /dev/null +++ b/test-apps/hello-world/src/main/java/io/github/pixee/testapp/Main.java @@ -0,0 +1,15 @@ +package io.github.pixee.testapp; + +import io.github.pixee.security.HostValidator; + +public final class Main { + + public static void main(final String[] args) { + String message = "Hello, World!"; + if (HostValidator.DENY_COMMON_INFRASTRUCTURE_TARGETS.isAllowed(message)) { + System.out.println(message); + } else { + System.out.println("Access denied"); + } + } +}