Skip to content

Commit 2fae022

Browse files
authored
Close SQL injection bug in BACKUP command (#35)
1 parent 4711ba1 commit 2fae022

File tree

2 files changed

+71
-13
lines changed

2 files changed

+71
-13
lines changed

src/main/scala/fr/brouillard/gitbucket/h2/controller/H2BackupController.scala

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package fr.brouillard.gitbucket.h2.controller
33
import java.io.File
44
import java.util.Date
55
import fr.brouillard.gitbucket.h2._
6-
import fr.brouillard.gitbucket.h2.controller.H2BackupController.{defaultBackupFileName, doBackup}
6+
import fr.brouillard.gitbucket.h2.controller.H2BackupController.{defaultBackupFileName, doBackup, exportConnectedDatabase, logger}
77
import gitbucket.core.controller.ControllerBase
88
import gitbucket.core.model.Account
99
import gitbucket.core.util.AdminAuthenticator
@@ -13,7 +13,13 @@ import org.scalatra.{ActionResult, Ok, Params}
1313
import org.slf4j.LoggerFactory
1414
import org.scalatra.forms._
1515

16+
import java.sql.Connection
17+
import scala.util.Using
18+
1619
object H2BackupController {
20+
21+
private val logger = LoggerFactory.getLogger(classOf[H2BackupController])
22+
1723
def defaultBackupFileName(): String = {
1824
val format = new java.text.SimpleDateFormat("yyyy-MM-dd_HH-mm")
1925
"gitbucket-db-" + format.format(new Date()) + ".zip"
@@ -28,28 +34,32 @@ object H2BackupController {
2834
case _ => org.scalatra.Unauthorized()
2935
}
3036
}
37+
38+
def exportConnectedDatabase(conn: Connection, exportFile: File): Unit = {
39+
val destFile = if (exportFile.isAbsolute) exportFile else new File(GitBucketHome + "/backup", exportFile.toString)
40+
41+
logger.info("Exporting database to {}", destFile)
42+
43+
Using.resource(conn.prepareStatement("BACKUP TO ?")){ statement =>
44+
statement.setString(1, destFile.toString)
45+
statement.execute()
46+
}
47+
48+
logger.info("Exported {} bytes.", exportFile.length())
49+
}
50+
3151
}
3252

3353
class H2BackupController extends ControllerBase with AdminAuthenticator {
34-
private val logger = LoggerFactory.getLogger(classOf[H2BackupController])
3554

3655
case class BackupForm(destFile: String)
3756

3857
private val backupForm = mapping(
3958
"dest" -> trim(label("Destination", text(required)))
4059
)(BackupForm.apply)
4160

42-
// private val defaultBackupFile:String = new File(GitBucketHome, "gitbucket-database-backup.zip").toString;
43-
4461
def exportDatabase(exportFile: File): Unit = {
45-
val destFile = if (exportFile.isAbsolute) exportFile else new File(GitBucketHome + "/backup", exportFile.toString)
46-
47-
val session = Database.getSession(request)
48-
val conn = session.conn
49-
50-
logger.info("exporting database to {}", destFile)
51-
52-
conn.prepareStatement("BACKUP TO '" + destFile + "'").execute()
62+
exportConnectedDatabase(Database.getSession(request).conn, exportFile)
5363
}
5464

5565
get("/admin/h2backup")(adminOnly {

src/test/scala/fr/brouillard/gitbucket/h2/controller/H2BackupControllerTests.scala

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,18 @@ package fr.brouillard.gitbucket.h2.controller
22

33
import gitbucket.core.model.Account
44
import gitbucket.core.servlet.ApiAuthenticationFilter
5+
import org.apache.commons.io.FileSystemUtils
6+
import org.h2.Driver
7+
import org.h2.engine.Database
58
import org.scalatest.funsuite.AnyFunSuite
69
import org.scalatest.matchers.should.Matchers.{convertToAnyShouldWrapper, equal}
710
import org.scalatra.{Ok, Params, ScalatraParams}
811
import org.scalatra.test.scalatest.ScalatraFunSuite
912

1013
import java.io.File
11-
import java.util.Date
14+
import java.nio.file.{Files, Path, Paths}
15+
import java.util.{Date, Properties}
16+
import scala.util.Using
1217

1318
class H2BackupControllerTests extends ScalatraFunSuite {
1419
addFilter(classOf[ApiAuthenticationFilter], path="/api/*")
@@ -59,6 +64,49 @@ class H2BackupControllerObjectTests extends AnyFunSuite {
5964
description = None)
6065
}
6166

67+
private def h2Url(file: File): String = {
68+
"jdbc:h2:file:" + file + ";DATABASE_TO_UPPER=false"
69+
}
70+
71+
test("exports connected database with safe file name") {
72+
exportsConnectedDatabase("backup.zip")
73+
}
74+
75+
test("exports connected database with unsafe file name") {
76+
exportsConnectedDatabase("data.zip' drop database xyx")
77+
}
78+
79+
private def exportsConnectedDatabase(backupFileName: String): Unit = {
80+
val tempDir = Files.createTempDirectory(classOf[H2BackupControllerObjectTests].getName + "-exports-connected-database")
81+
try {
82+
val requestedDbFile = new File(tempDir.toFile, "data")
83+
// H2 can create several files; in this case, it will only create a data file and no lock files.
84+
val createdDbFile = new File(tempDir.toFile, "data.mv.db")
85+
val backup = new File(tempDir.toFile, backupFileName)
86+
87+
val driver = new Driver()
88+
val conn = driver.connect(h2Url(requestedDbFile), new Properties());
89+
try {
90+
assert(createdDbFile.exists())
91+
92+
H2BackupController.exportConnectedDatabase(conn, backup)
93+
try {
94+
assert(backup.length() > 0)
95+
}
96+
finally {
97+
assert(backup.delete())
98+
}
99+
}
100+
finally {
101+
conn.close()
102+
assert(createdDbFile.delete())
103+
}
104+
}
105+
finally {
106+
assert(tempDir.toFile.delete())
107+
}
108+
}
109+
62110
test("generates default file name") {
63111
assertDefaultFileName(H2BackupController.defaultBackupFileName())
64112
}

0 commit comments

Comments
 (0)