Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SecurityManager support to block suspicious code #622 #625

Merged
merged 2 commits into from
Aug 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ data class UtOverflowFailure(
override val exception: Throwable,
) : UtExecutionFailure()

data class UtSandboxFailure(
override val exception: Throwable
) : UtExecutionFailure()

/**
* unexpectedFail (when exceptions such as NPE, IOBE, etc. appear, but not thrown by a user, applies both for function under test and nested calls )
* expectedCheckedThrow (when function under test or nested call explicitly says that checked exception could be thrown and throws it)
Expand Down
13 changes: 7 additions & 6 deletions utbot-framework/src/main/kotlin/org/utbot/engine/Resolver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ import kotlin.math.min
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentSetOf
import org.utbot.framework.plugin.api.SYMBOLIC_NULL_ADDR
import org.utbot.framework.plugin.api.UtSandboxFailure
import soot.ArrayType
import soot.BooleanType
import soot.ByteType
Expand All @@ -88,6 +89,7 @@ import soot.SootClass
import soot.SootField
import soot.Type
import soot.VoidType
import java.security.AccessControlException

// hack
const val MAX_LIST_SIZE = 10
Expand Down Expand Up @@ -371,12 +373,11 @@ class Resolver(
return if (explicit) {
UtExplicitlyThrownException(exception, inNestedMethod)
} else {
// TODO SAT-1561
val isOverflow = exception is ArithmeticException && exception.message?.contains("overflow") == true
if (isOverflow) {
UtOverflowFailure(exception)
} else {
UtImplicitlyThrownException(exception, inNestedMethod)
when {
// TODO SAT-1561
exception is ArithmeticException && exception.message?.contains("overflow") == true -> UtOverflowFailure(exception)
exception is AccessControlException -> UtSandboxFailure(exception)
else -> UtImplicitlyThrownException(exception, inNestedMethod)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ import org.utbot.framework.plugin.api.UtNewInstanceInstrumentation
import org.utbot.framework.plugin.api.UtNullModel
import org.utbot.framework.plugin.api.UtPrimitiveModel
import org.utbot.framework.plugin.api.UtReferenceModel
import org.utbot.framework.plugin.api.UtSandboxFailure
import org.utbot.framework.plugin.api.UtStaticMethodInstrumentation
import org.utbot.framework.plugin.api.UtSymbolicExecution
import org.utbot.framework.plugin.api.UtTimeoutException
Expand Down Expand Up @@ -141,6 +142,7 @@ import org.utbot.framework.plugin.api.util.wrapIfPrimitive
import org.utbot.framework.util.isUnit
import org.utbot.summary.SummarySentenceConstants.TAB
import java.lang.reflect.InvocationTargetException
import java.security.AccessControlException
import java.lang.reflect.ParameterizedType
import kotlin.reflect.jvm.javaType

Expand Down Expand Up @@ -351,6 +353,11 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c
methodType = CRASH
writeWarningAboutCrash()
}
is AccessControlException -> {
methodType = CRASH
writeWarningAboutFailureTest(exception)
return
}
else -> {
methodType = FAILING
writeWarningAboutFailureTest(exception)
Expand All @@ -361,6 +368,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c
}

private fun shouldTestPassWithException(execution: UtExecution, exception: Throwable): Boolean {
if (exception is AccessControlException) return false
// tests with timeout or crash should be processed differently
if (exception is TimeoutException || exception is ConcreteExecutionFailureException) return false

Expand Down Expand Up @@ -1533,6 +1541,12 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c
)
}

if (result is UtSandboxFailure) {
testFrameworkManager.disableTestMethod(
"Disabled due to sandbox"
)
}

val testMethod = buildTestMethod {
name = methodName
parameters = params
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import org.utbot.framework.plugin.api.UtInstrumentation
import org.utbot.framework.plugin.api.UtMethod
import org.utbot.framework.plugin.api.UtModel
import org.utbot.framework.plugin.api.UtNewInstanceInstrumentation
import org.utbot.framework.plugin.api.UtSandboxFailure
import org.utbot.framework.plugin.api.UtStaticMethodInstrumentation
import org.utbot.framework.plugin.api.UtTimeoutException
import org.utbot.framework.plugin.api.util.UtContext
Expand All @@ -37,6 +38,7 @@ import org.utbot.instrumentation.instrumentation.et.ExplicitThrowInstruction
import org.utbot.instrumentation.instrumentation.et.TraceHandler
import org.utbot.instrumentation.instrumentation.instrumenter.Instrumenter
import org.utbot.instrumentation.instrumentation.mock.MockClassVisitor
import java.security.AccessControlException
import java.security.ProtectionDomain
import java.util.IdentityHashMap
import kotlin.reflect.jvm.javaMethod
Expand Down Expand Up @@ -221,6 +223,9 @@ object UtExecutionInstrumentation : Instrumentation<UtConcreteExecutionResult> {
if (exception is TimeoutException) {
return UtTimeoutException(exception)
}
if (exception is AccessControlException) {
return UtSandboxFailure(exception)
}
val instrs = traceHandler.computeInstructionList()
val isNested = if (instrs.isEmpty()) {
false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package org.utbot.instrumentation.instrumentation

import org.utbot.common.withAccessibility
import org.utbot.framework.plugin.api.util.signature
import org.utbot.instrumentation.process.sandbox
import java.lang.reflect.Constructor
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method
Expand Down Expand Up @@ -54,7 +55,7 @@ class InvokeInstrumentation : Instrumentation<Result<*>> {
is Method ->
withAccessibility {
runCatching {
invoke(thisObject, *realArgs.toTypedArray()).let {
sandbox { invoke(thisObject, *realArgs.toTypedArray()) }.let {
if (returnType != Void.TYPE) it else Unit
} // invocation on method returning void will return null, so we replace it with Unit
}
Expand All @@ -63,7 +64,7 @@ class InvokeInstrumentation : Instrumentation<Result<*>> {
is Constructor<*> ->
withAccessibility {
runCatching {
newInstance(*realArgs.toTypedArray())
sandbox { newInstance(*realArgs.toTypedArray()) }
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import java.io.File
import java.io.OutputStream
import java.io.PrintStream
import java.net.URLClassLoader
import java.security.AllPermission
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import kotlin.system.exitProcess
Expand Down Expand Up @@ -51,6 +52,12 @@ private val kryoHelper: KryoHelper = KryoHelper(System.`in`, System.`out`)
* It should be compiled into separate jar file (child_process.jar) and be run with an agent (agent.jar) option.
*/
fun main() {
permissions {
// Enable all permissions for instrumentation.
// SecurityKt.sandbox() is used to restrict these permissions.
+ AllPermission()
}

// We don't want user code to litter the standard output, so we redirect it.
val tmpStream = PrintStream(object : OutputStream() {
override fun write(b: Int) {}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
@file:Suppress("JAVA_MODULE_DOES_NOT_EXPORT_PACKAGE")

package org.utbot.instrumentation.process

import sun.security.provider.PolicyFile
import java.net.URI
import java.nio.file.Files
import java.nio.file.Paths
import java.security.AccessControlContext
import java.security.AccessController
import java.security.CodeSource
import java.security.Permission
import java.security.PermissionCollection
import java.security.Permissions
import java.security.Policy
import java.security.PrivilegedAction
import java.security.PrivilegedActionException
import java.security.ProtectionDomain
import java.security.cert.Certificate

internal fun permissions(block: SimplePolicy.() -> Unit) {
val policy = Policy.getPolicy()
if (policy !is SimplePolicy) {
Policy.setPolicy(SimplePolicy(block))
System.setSecurityManager(SecurityManager())
} else {
policy.block()
}
}

/**
* Run [block] in sandbox mode.
*
* When running in sandbox by default only necessary to instrumentation permissions are enabled.
* Other options are not enabled by default and rises [java.security.AccessControlException].
*
* To add new permissions create and/or edit file "{user.home}/.utbot/sandbox.policy".
*
* For example to enable property reading (`System.getProperty("user.home")`):
*
* ```
* grant {
* permission java.util.PropertyPermission "user.home", "read";
* };
* ```
* Read more [about policy file and syntax](https://docs.oracle.com/javase/7/docs/technotes/guides/security/PolicyFiles.html#Examples)
*/
internal fun <T> sandbox(block: () -> T): T {
val policyPath = Paths.get(System.getProperty("user.home"), ".utbot", "sandbox.policy")
return sandbox(policyPath.toUri()) { block() }
}

internal fun <T> sandbox(file: URI, block: () -> T): T {
val path = Paths.get(file)
val perms = mutableListOf<Permission>(
RuntimePermission("accessDeclaredMembers")
)
val allCodeSource = CodeSource(null, emptyArray<Certificate>())
if (Files.exists(path)) {
val policyFile = PolicyFile(file.toURL())
val collection = policyFile.getPermissions(allCodeSource)
perms += collection.elements().toList()
}
return sandbox(perms, allCodeSource) { block() }
}

internal fun <T> sandbox(permission: List<Permission>, cs: CodeSource, block: () -> T): T {
val perms = permission.fold(Permissions()) { acc, p -> acc.add(p); acc }
return sandbox(perms, cs) { block() }
}

internal fun <T> sandbox(perms: PermissionCollection, cs: CodeSource, block: () -> T): T {
val acc = AccessControlContext(arrayOf(ProtectionDomain(cs, perms)))
return try {
AccessController.doPrivileged(PrivilegedAction { block() }, acc)
} catch (e: PrivilegedActionException) {
throw e.exception
}
}

/**
* This policy can add grant or denial rules for permissions.
*
* To add a grant permission use like this in any place:
*
* ```
* permissions {
* + java.security.PropertyPolicy("user.home", "read,write")
* }
* ```
*
* After first call [SecurityManager] is set with this policy
*
* To deny a permission:
*
* ```
* permissions {
* - java.security.PropertyPolicy("user.home", "read,write")
* }
* ```
*
* To delete all concrete permissions (if it was added before):
*
* ```
* permissions {
* ! java.security.PropertyPolicy("user.home", "read,write")
* }
* ```
*
* The last permission has priority. Enable all property read for "user.*", but forbid to read only "user.home":
*
* ```
* permissions {
* + java.security.PropertyPolicy("user.*", "read,write")
* - java.security.PropertyPolicy("user.home", "read,write")
* }
* ```
*/
internal class SimplePolicy(init: SimplePolicy.() -> Unit = {}) : Policy() {
sealed class Access(val permission: Permission) {
class Allow(permission: Permission) : Access(permission)
class Deny(permission: Permission) : Access(permission)
}
private var permissions = mutableListOf<Access>()

init { apply(init) }

operator fun Permission.unaryPlus() = permissions.add(Access.Allow(this))

operator fun Permission.unaryMinus() = permissions.add(Access.Deny(this))

operator fun Permission.not() = permissions.removeAll { it.permission == this }

override fun getPermissions(codesource: CodeSource) = UNSUPPORTED_EMPTY_COLLECTION!!
override fun getPermissions(domain: ProtectionDomain) = UNSUPPORTED_EMPTY_COLLECTION!!
override fun implies(domain: ProtectionDomain, permission: Permission): Boolean {
// 0 means no info, < 0 is denied and > 0 is allowed
val result = permissions.lastOrNull { it.permission.implies(permission) }?.let {
when (it) {
is Access.Allow -> 1
is Access.Deny -> -1
}
} ?: 0
return result > 0
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package org.utbot.instrumentation.security

import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.utbot.instrumentation.process.permissions
import org.utbot.instrumentation.process.sandbox
import java.lang.NullPointerException
import java.security.AccessControlException
import java.security.AllPermission
import java.security.BasicPermission
import java.security.CodeSource
import java.security.Permission
import java.security.cert.Certificate
import java.util.PropertyPermission

class SecurityTest {

@BeforeEach
fun init() {
permissions {
+AllPermission()
}
}

@Test
fun `basic security works`() {
sandbox {
assertThrows<AccessControlException> {
System.getProperty("any")
}
}
}

@Test
fun `basic permission works`() {
sandbox(listOf(PropertyPermission("java.version", "read")), CodeSource(null, emptyArray<Certificate>())) {
val result = System.getProperty("java.version")
assertNotNull(result)
assertThrows<AccessControlException> {
System.setProperty("any_random_value_key", "random")
}
}
}

@Test
fun `null is ok`() {
val empty = object : BasicPermission("*") {}
val field = Permission::class.java.getDeclaredField("name")
field.isAccessible = true
field.set(empty, null)
val collection = empty.newPermissionCollection()
assertThrows<NullPointerException> {
collection.implies(empty)
}
}

}
Loading