Skip to content

Commit

Permalink
Do not allow entity references to refer to entities
Browse files Browse the repository at this point in the history
  • Loading branch information
dpp committed Mar 12, 2015
1 parent b578ab1 commit c2dc7e5
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 18 deletions.
66 changes: 52 additions & 14 deletions core/util/src/main/scala/net/liftweb/util/SecurityHelpers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,21 @@
package net.liftweb
package util

import javax.xml.XMLConstants
import javax.xml.parsers.SAXParserFactory
import javax.xml.transform.stream.StreamSource
import javax.xml.validation.SchemaFactory

import org.apache.commons.codec.binary.Base64
import java.io.{InputStream, ByteArrayOutputStream, ByteArrayInputStream, Reader, File, FileInputStream, BufferedReader}
import java.security.{SecureRandom, MessageDigest}
import java.io._
import java.security._
import javax.crypto._
import javax.crypto.spec._
import scala.xml.{Node, XML}
import org.xml.sax.{EntityResolver, InputSource, ErrorHandler}
import org.xml.sax.helpers.DefaultHandler

import scala.xml.parsing.NoBindingFactoryAdapter
import scala.xml.{TopScope, Elem, Node, XML}
import common._

object SecurityHelpers extends StringHelpers with IoHelpers with SecurityHelpers
Expand Down Expand Up @@ -75,23 +84,23 @@ trait SecurityHelpers {
def md5(in: String): String = base64Encode(md5(in.getBytes("UTF-8")))

/** create a SHA hash from a Byte array */
def hash(in : Array[Byte]) : Array[Byte] = {
def hash(in: Array[Byte]): Array[Byte] = {
MessageDigest.getInstance("SHA").digest(in)
}

/** create a SHA hash from a String */
def hash(in: String) : String = {
def hash(in: String): String = {
base64Encode(MessageDigest.getInstance("SHA").digest(in.getBytes("UTF-8")))
}

/** create a SHA hash from a String */
def hashHex(in: String) : String = {
/** create a SHA hash from a String */
def hashHex(in: String): String = {
Helpers.hexEncode(MessageDigest.getInstance("SHA").digest(in.getBytes("UTF-8")))
}

/** Compare two strings in a way that does not vary if the strings
* are determined to be not equal early (test every byte... avoids
* timing attackes */
* are determined to be not equal early (test every byte... avoids
* timing attackes */
def secureEquals(s1: String, s2: String): Boolean = (s1, s2) match {
case (null, null) => true
case (null, _) => false
Expand All @@ -100,8 +109,8 @@ trait SecurityHelpers {
}

/** Compare two byte arrays in a way that does not vary if the arrays
* are determined to be not equal early (test every byte... avoids
* timing attackes */
* are determined to be not equal early (test every byte... avoids
* timing attackes */
def secureEquals(s1: Array[Byte], s2: Array[Byte]): Boolean = (s1, s2) match {
case (null, null) => true
case (null, _) => false
Expand All @@ -121,12 +130,12 @@ trait SecurityHelpers {


/** create a SHA-256 hash from a Byte array */
def hash256(in : Array[Byte]) : Array[Byte] = {
def hash256(in: Array[Byte]): Array[Byte] = {
MessageDigest.getInstance("SHA-256").digest(in)
}

/** create a SHA-256 hash from a String */
def hash256(in : String): String = {
def hash256(in: String): String = {
base64Encode(MessageDigest.getInstance("SHA-256").digest(in.getBytes("UTF-8")))
}

Expand Down Expand Up @@ -164,7 +173,7 @@ trait SecurityHelpers {
case 'd' | 'D' => 13
case 'e' | 'E' => 14
case 'f' | 'F' => 15
case _ => 0
case _ => 0
}

while (pos < max) {
Expand Down Expand Up @@ -197,5 +206,34 @@ trait SecurityHelpers {
sb.toString
}

def secureParseXML(in: String): Elem = secureParseXML(new StringReader(in))

def secureParseXML(in: Reader): Elem = {
val spf = SAXParserFactory.newInstance()
val saxParser = spf.newSAXParser();
val is = new InputSource(in);
val newAdapter = new SecureFactoryAdaptor
newAdapter.scopeStack.push( TopScope)

saxParser.parse(is, newAdapter)

newAdapter.scopeStack.pop
newAdapter.rootElem.asInstanceOf[Elem]
}

def secureParseXML(in: InputStream): Elem = secureParseXML(new InputStreamReader(in, "UTF-8"))


def tryIt = secureParseXML("""<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >]><foo>&xxe;</foo>""")

}

private class SecureFactoryAdaptor extends NoBindingFactoryAdapter {
override def resolveEntity(publicId: String, systemId: String): InputSource = {
throw new SecurityException("Attempt to access an entity "+publicId+" / "+systemId)

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ object BindHelpersSpec extends Specification {
"handle dynamic, unprefixed attributes" in {
// The Unprefixed attributes that Lift merges in cause the XML equals comparison to fail
// stringifying and then reparsing fixes it.
XML.loadString(
Helpers.secureParseXML(
BindHelpers.bind("test",
<div><div test:x="dynamicUnprefixed" /></div>,
FuncAttrBindParam("x", {ns : NodeSeq => ns }, "id")).toString) must ==/(<div><div id="dynamicUnprefixed" /></div>)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,27 @@ object SecurityHelpersSpec extends Specification {
"SecurityHelpers Specification".title

"Security Helpers" should {
"Parse XML with nasty entity references" in {
(try {
Helpers.secureParseXML( """<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >]><foo>&xxe;</foo>""")
} catch {
case e: SecurityException => "bad"
}) must_== "bad"
}

"Parse XML with deal with nice entity references" in {
(try { Helpers.secureParseXML("""<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >]><foo>&amp;</foo>""")
} catch {
case e: SecurityException => "yes"
}).toString must_== "<foo>&amp;</foo>"
}

"provide a randomLong method returning a random Long modulo a number" in {
randomLong(7L) must be_<(7L)
}
Expand Down
2 changes: 1 addition & 1 deletion web/webkit/src/main/scala/net/liftweb/http/Req.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1021,7 +1021,7 @@ class Req(val path: ParsePath,
else
try {
import java.io._
body.map(b => XML.load(new ByteArrayInputStream(b)))
body.map(b => Helpers.secureParseXML(new ByteArrayInputStream(b)))
} catch {
case e: LiftFlowOfControlException => throw e
case e: Exception => Failure(e.getMessage, Full(e), Empty)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
package net.liftweb
package builtin.snippet

import net.liftweb.util.Helpers

import xml._
import org.specs2.mutable.Specification

Expand All @@ -43,7 +45,7 @@ object MsgSpec extends Specification {

// We reparse due to inconsistencies with UnparsedAttributes
val result = S.withAttrs(new UnprefixedAttribute("id", Text("foo"), new UnprefixedAttribute("noticeClass", Text("funky"), Null))) {
XML.loadString(Msg.render(<div/>).toString)
Helpers.secureParseXML(Msg.render(<div/>).toString)
}

result must ==/(<span id="foo">Error, <span class="funky">Notice</span></span>)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
package net.liftweb
package builtin.snippet

import net.liftweb.util.Helpers

import xml.XML
import org.specs2.mutable.Specification

Expand All @@ -42,7 +44,7 @@ object MsgsSpec extends Specification {
S.notice("Notice")

// We reparse due to inconsistencies with UnparsedAttributes
XML.loadString(Msgs.render(
Helpers.secureParseXML(Msgs.render(
<lift:warning_msg>Warning:</lift:warning_msg><lift:notice_class>funky</lift:notice_class>
).toString)
}
Expand Down

0 comments on commit c2dc7e5

Please sign in to comment.