Skip to content

Commit

Permalink
enhanced Duga command security
Browse files Browse the repository at this point in the history
  • Loading branch information
Zomis committed Jun 21, 2015
1 parent e116657 commit a16563b
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 16 deletions.
31 changes: 25 additions & 6 deletions src/main/groovy/net/zomis/duga/tasks/ChatCommandDelegate.groovy
Original file line number Diff line number Diff line change
@@ -1,34 +1,53 @@
package net.zomis.duga.tasks

import groovy.transform.CompileStatic
import groovy.transform.TypeCheckingMode
import net.zomis.duga.DugaChatListener
import net.zomis.duga.User
import net.zomis.duga.chat.WebhookParameters

import java.util.concurrent.Callable

/**
* Delegate for running chat commands
*/
class ChatCommandDelegate {
abstract class ChatCommandDelegate extends Script {

private ChatMessageIncoming message
private DugaChatListener bean

private final ChatMessageIncoming message
private final DugaChatListener bean
ChatCommandDelegate() {
// this constructor intentionally left blank
}

ChatCommandDelegate(ChatMessageIncoming chatMessageIncoming, DugaChatListener bean) {
this.message = chatMessageIncoming
void init(ChatMessageIncoming message, DugaChatListener bean) {
if (this.message || this.bean) {
throw new IllegalStateException('message and bean can only be initialized once')
}
this.message = message
this.bean = bean
}

Closure ping = {
message.reply('pong!')
}

def say(String text) {
Map say(String text) {
[inRoom: {int id ->
bean.chatBot.postSingle(WebhookParameters.toRoom(Integer.toString(id)), text)
}, default: {
message.reply(text)
}]
}

ChatMessageIncoming getMessage() {
message
}

DugaChatListener getBean() {
bean
}

def register(String githubKey) {
User.withNewSession {status ->
User user = User.findByPingExpect(githubKey)
Expand Down
35 changes: 25 additions & 10 deletions src/main/groovy/net/zomis/duga/tasks/ListenTask.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,22 @@ import com.gistlabs.mechanize.document.json.JsonDocument
import com.gistlabs.mechanize.document.json.node.JsonNode
import com.gistlabs.mechanize.impl.MechanizeAgent
import groovy.json.JsonSlurper
import groovy.transform.CompileStatic
import groovy.transform.TimedInterrupt
import net.zomis.duga.ChatCommands
import net.zomis.duga.DugaBot
import net.zomis.duga.DugaChatListener
import net.zomis.duga.chat.WebhookParameters
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer
import org.codehaus.groovy.control.customizers.SecureASTCustomizer

import static org.codehaus.groovy.syntax.Types.*

class ListenTask implements Runnable {

private static final int NUM_MESSAGES = 10
private static final String DUGA_COMMAND = '@Duga do '

private final DugaBot bot
private final String room
Expand Down Expand Up @@ -49,6 +53,7 @@ class ListenTask implements Runnable {
// the list of tokens the user can find
// constants are defined in org.codehaus.groovy.syntax.Types
tokensWhitelist = [
ASSIGN,
PLUS, MINUS, MULTIPLY, DIVIDE, MOD,
POWER, PLUS_PLUS, MINUS_MINUS, COMPARE_EQUAL,
COMPARE_NOT_EQUAL, COMPARE_LESS_THAN, COMPARE_LESS_THAN_EQUAL,
Expand All @@ -57,7 +62,7 @@ class ListenTask implements Runnable {
// limit the types of constants that a user can define to number types only
constantTypesClassesWhiteList = [
String,
Object,
Object, // simply typing `ping` to invoke ChatCommandDelegate.ping requires this
Integer,
Float,
Long,
Expand All @@ -71,7 +76,7 @@ class ListenTask implements Runnable {
// method calls are only allowed if the receiver is of one of those types
// be careful, it's not a runtime type!
receiversClassesWhiteList = [
Object,
Object, // compiler believes that any method call is a call on Object, it is not aware of the delegate class
Math,
Integer,
Float,
Expand All @@ -81,7 +86,14 @@ class ListenTask implements Runnable {
].asImmutable()
}
cc.addCompilationCustomizers(scz)
cc.setScriptBaseClass(DelegatingScript.class.getName())
def compileStaticCustomizer = new ASTTransformationCustomizer(Collections.singletonMap('extensions',
Collections.singletonList('typecheck-extension.groovy')), CompileStatic.class)
cc.addCompilationCustomizers(compileStaticCustomizer)
Map timeoutOptions = new HashMap()
timeoutOptions.put('value', 5)
def timeoutCustomizer = new ASTTransformationCustomizer(timeoutOptions, TimedInterrupt.class)
cc.addCompilationCustomizers(timeoutCustomizer)
cc.setScriptBaseClass(ChatCommandDelegate.class.getName())
this.groovyShell = new GroovyShell(getClass().getClassLoader(), binding, cc)
}

Expand Down Expand Up @@ -121,15 +133,18 @@ class ListenTask implements Runnable {
continue
}
def content = message.content
if (!content.startsWith('@Duga ')) {
if (!content.startsWith(DUGA_COMMAND)) {
continue
}
if (!authorizedCommander(message)) {
continue
}
message.cleanHTML()
println "possible command: $message.content"
botCommand(message)
def commandResult = botCommand(message)
if (commandResult != null) {
message.reply('Result: ' + String.valueOf(commandResult))
}
// handler.botCommand(message)
}
/* Root node: {"ms":4,"time":41194973,"sync":1433551091,"events":
Expand All @@ -143,11 +158,9 @@ class ListenTask implements Runnable {
}

def botCommand(ChatMessageIncoming chatMessageIncoming) {
def delegate = new ChatCommandDelegate(chatMessageIncoming, bean)

try {
DelegatingScript script = (DelegatingScript) groovyShell.parse(chatMessageIncoming.content.substring('@Duga '.length()))
script.setDelegate(delegate)
ChatCommandDelegate script = (ChatCommandDelegate) groovyShell.parse(chatMessageIncoming.content.substring(DUGA_COMMAND.length()))
script.init(chatMessageIncoming, bean)
def result = script.run()
println 'Script ' + chatMessageIncoming + ' returned ' + result
if (result instanceof Map) {
Expand All @@ -165,9 +178,11 @@ class ListenTask implements Runnable {
return result
} catch (Exception ex) {
println 'Script ' + chatMessageIncoming + ' caused exception: ' + ex
String mess = ex.toString()
chatMessageIncoming.reply(mess.substring(0, Math.min(420, mess.length())))
// bot.postDebug(chatMessageIncoming.toString() + ' caused exception: ' + ex)
ex.printStackTrace(System.out)
return ex
return null
}
}

Expand Down
21 changes: 21 additions & 0 deletions src/main/resources/typecheck-extension.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
beforeMethodCall { call ->
if (isMethodCallExpression(call)) {
['clone', 'finalize', '']
def methodName = call.methodAsString
if (methodName == 'wait' ||
methodName == 'clone' ||
methodName == 'finalize' ||
methodName == 'notify' ||
methodName == 'notifyAll') {
addStaticTypeError('Not allowed',call)
handled = true
}
}
}

methodNotFound { receiver, name, argList, argTypes, call ->
if (receiver.name == 'java.util.Map') {
handled = true
makeDynamic(call, classNodeFor(Map))
}
}

0 comments on commit a16563b

Please sign in to comment.