JDBC中可能存在的错误?

时间:2014-12-29 23:36:04

标签: scala playframework anorm

我目前面临一个奇怪的问题。 每当用户在搜索栏中输入以“s”开头的内容时,请求就会崩溃。 您接下来看到的是我为此项目编写的搜索引擎生成的示例sql代码。

    SELECT Profiles.ProfileID,Profiles.Nickname,Profiles.Email,Profiles.Status,Profiles.Role,Profiles.Credits, Profiles.Language,Profiles.Created,Profiles.Modified,Profiles.Cover,Profiles.Prename, Profiles.Lastname,Profiles.BirthDate,Profiles.Country,Profiles.City,Profiles.Phone,Profiles.Website, Profiles.Description, Profiles.Affair,Scores.AvgScore, coalesce(Scores.NumScore, 0) AS NumScore, coalesce(Scores.NumScorer, 0) AS NumScorer, (
    (SELECT count(*)
     FROM Likes
     JOIN Comments using(CommentID)
     WHERE Comments.ProfileID = Profiles.ProfileID)) NumLikes, (
(SELECT count(*)
  FROM Likes
 JOIN Comments using(CommentID)
  WHERE Comments.ProfileID = Profiles.ProfileID) /
  (SELECT coalesce(nullif(count(*), 0), 1)
 FROM Comments
 WHERE Comments.ProfileID = Profiles.ProfileID)) AvgLikes,                                                                                                             Movies.MovieID,                                                                                                        Movies.Caption,                                                                                                               Movies.Description,                                                                                                              Movies.Language,                                                                                                               Movies.Country,                                                                                                             Movies.City,                                                                                                             Movies.Kind,                                                                                                             Movies.Integration,  
    (SELECT cast(least(25 + 5.000000 * round((75 * ((0.500000 * SIZE/1024.0/1024.0 * 0.001250) + (0.500000 * Duration/60.0 * 0.050000))) / 5.000000), 100) AS signed int)
     FROM Streams
     WHERE MovieID = Movies.MovieID
       AND Tag = "main"
       AND ENCODING = "mp4") AS ChargeMain,

    (SELECT cast(least(25 + 10.000000 * round((75 * ((0.200000 * SIZE/1024.0/1024.0 * 0.001000) + (0.800000 * Duration/60.0 * 0.016667))) / 10.000000), 100) AS signed int)
     FROM Streams
     WHERE MovieID = Movies.MovieID
       AND Tag = "notes"
       AND ENCODING = "mp4") AS ChargeNotes,

    (SELECT coalesce(count(*), 0)
     FROM Views
     WHERE Views.MovieID = Movies.MovieID
       AND Tag = "main") AS MainViews,

    (SELECT coalesce(count(*), 0)
     FROM Views
     WHERE Views.MovieID = Movies.MovieID
       AND Tag = "notes") AS NotesViews,

    (SELECT coalesce(count(*), 0)
     FROM Views
     WHERE Views.MovieID = Movies.MovieID
       AND Tag = "trailer") AS TrailerViews,

    (SELECT coalesce(greatest(
(SELECT coalesce(count(*), 0)
 FROM Views
 WHERE Views.MovieID = Movies.MovieID
   AND Tag = "trailer"),
 (SELECT coalesce(count(*), 0)
 FROM Views
  WHERE Views.MovieID = Movies.MovieID
 AND Tag = "main")), 0)) AS MaxMainTrailerViews,
    (SELECT avg(Score)
     FROM Scores
     WHERE Scores.MovieID = Movies.MovieID) AS Score,
    (SELECT coalesce(group_concat(cast(Score AS signed int)), "")
     FROM Scores
     WHERE Scores.MovieID = Movies.MovieID) AS Scores,                                                                                                          Movies.Cover,                                                                                                            Movies.Locked,                                                                                                             Movies.Created,                                                                                                           Movies.Modified,

    (SELECT coalesce(group_concat(name separator ','),"")
     FROM Tags
     JOIN TagLinks using(TagID)
     WHERE TagLinks.MovieID = Movies.MovieID
     ORDER BY name ASC) AS Tags,

    (SELECT count(*)
     FROM Purchases
     WHERE MovieID = Movies.MovieID
       AND ProfileID = %s
       AND TYPE = "main") AS PurchasedMain,

    (SELECT count(*)
     FROM Purchases
     WHERE MovieID = Movies.MovieID
       AND ProfileID = %s
       AND TYPE = "notes") AS PurchasedNotes,

    (SELECT count(*)
     FROM Watchlist
     WHERE MovieID = Movies.MovieID
       AND ProfileID = %s) AS Watchlist,

    (SELECT count(*)
     FROM Scores
     WHERE MovieID = Movies.MovieID
       AND ProfileID = %s) AS Rated,

    (SELECT count(*)
     FROM Comments
     WHERE MovieID = Movies.MovieID
       AND Deleted IS NULL) AS Comments,

    (SELECT sum(Duration)
     FROM Streams
     WHERE Streams.MovieID = Movies.MovieID
       AND Streams.Tag IN ("main",
                           "notes")
       AND Streams.ENCODING = "mp4") AS Runtime,

    (SELECT cast(count(*) AS signed int)
     FROM Movies
     JOIN Profiles ON Profiles.ProfileID = Movies.ProfileID
     WHERE ((Movies.Locked = 0
             AND
               (SELECT count(*)
                FROM Streams
                WHERE Streams.MovieID = Movies.MovieID
                  AND Streams.Status <> "ready") = 0
             AND Profiles.Status = "active")
            OR (%s = 1)
            OR (Movies.ProfileID = %s))
        AS Movies,

    (SELECT cast(ceil(count(*) / %s) AS signed int)
     FROM Movies
     JOIN Profiles using(ProfileID)
     WHERE ((Movies.Locked = 0
             AND
               (SELECT count(*)
                FROM Streams
                WHERE Streams.MovieID = Movies.MovieID
                  AND Streams.Status <> "ready") = 0
             AND Profiles.Status = "active")
            OR (%s = 1)
            OR (Movies.ProfileID = %s))
        AS Pages
    FROM Movies
    JOIN Profiles using(ProfileID)
    LEFT JOIN
    (SELECT Movies.ProfileID AS ProfileID,
            avg(Scores.Score) AS AvgScore,
            count(*) AS NumScore,
            count(DISTINCT Scores.ProfileID) AS NumScorer
     FROM Scores
     JOIN Movies using(MovieID)
     GROUP BY Movies.ProfileID) AS Scores using(ProfileID)
    WHERE ((Movies.Locked = 0
            AND
              (SELECT count(*)
               FROM Streams
               WHERE Streams.MovieID = Movies.MovieID
                 AND Streams.Status <> "ready") = 0
            AND Profiles.Status = "active")
           OR (%s = 1)
           OR (Movies.ProfileID = %s))

    ORDER BY Score DESC LIMIT %s,
                            %s

经过无数个小时的调查和比较可能的用户输入与生成的sql代码后,我终于把问题解决了一些奇怪的JDBC驱动程序行为,我认为这是一个严重的错误 - 但我不确定:

我花了几个小时尝试用尽可能少的sql代码重现问题,最后得到以下内容:

SQL("""select * from Movies where "s" like "%s" and MovieID = {a} """)
.on('a -> 1).as(scalar[Long]*)

[SQLException:参数索引超出范围(1&gt;参数个数,为0)。]

SQL("""select * from Movies where "s" like "%samuel" and MovieID = {a} """)
.on('a -> 1).as(scalar[Long]*)

[SQLException:参数索引超出范围(1&gt;参数个数,为0)。]

SQL("""select * from Movies where "s" like "%flower" and MovieID = {a} """)
.on('a -> 1).as(scalar[Long]*)

[OK]

SQL("""select * from Movies where "s" like "%samuel" and MovieID = 1 """)
.on('a -> 1).as(scalar[Long]*)

[OK]

SQL("""select * from Movies where "s" like "%s" and MovieID = "{a}" """)
.on('a -> 1).as(scalar[Long]*)

[OK]

SQL("""select * from Movies where MovieID = {a} and "s" like "%s" """)
.on('a -> 1).as(scalar[Long]*)

[OK]

我相信在这里看到一种模式: 在sql代码中任何地方都有%s序列(引用或不引用)的确切条件下,后跟一个带有任意名称和任意距离的非引用命名参数 到%s序列,jdbc(或anorm)崩溃。崩溃似乎发生在JDBC中,但是Anorm也可能会向JDBC提交无效值。

你们有什么建议吗?

2 个答案:

答案 0 :(得分:0)

我认为我找到了一个持久的问题解决方案。由于我的sql生成器需要保持非常灵活,我不知何故需要一种方法来传递sql片段及其相应的参数,而无需立即评估它们。相反,生成器必须能够随时组装和组合各种sql片段成更大的片段 - 正如他现在所做的那样 - 但现在使用的是伴随的,尚未评估的参数。我想出了这个原型:

DB.withConnection("betterdating") { implicit connection =>
  case class SqlFragment(Fragment: String, Args: NamedParameter*)

  val aa = SqlFragment("select MovieID from Movies")
  val bb = SqlFragment("join Profiles using(ProfileID)")
  val cc = SqlFragment("where Caption like \"%{a}\" and MovieID = {b}", 'a -> "s", 'b -> 5)

  // combine all fragments
  val v1 = SQL(Seq(aa, bb, cc).map(_.Fragment).mkString(" "))
            .on((aa.Args ++ bb.Args ++ cc.Args): _*)

  // better solution
  val v2 = Seq(aa, bb, cc).unzip(frag => (frag.Fragment, frag.Args)) match {
    case (frags, args) => SQL(frags.mkString(" ")).on(args.flatten: _*)
  }

  // works
  println(v1.as(scalar[Long].singleOpt))
  println(v2.as(scalar[Long].singleOpt))
}

它看起来很棒! : - )

然后我重写了自由文本过滤器的最后一部分,如下所示:

// finally transform the expression
// list a single sql fragment
expressions.zipWithIndex.map { case (expr, index) =>
  s"""
    (concat(Movies.Caption, " ", Movies.Description, " ", Movies.Kind, " ", Profiles.Nickname, " ",
    (select coalesce(group_concat(Tags.Name), "") from Tags join TagLinks using (TagID)
    where TagLinks.MovieID = Movies.MovieID)) like "%{expr$index}%"))
  """ -> (s"expr$index" -> expr)
}.unzip match { case (frags, args) => SqlFragment(frags.mkString(" and "), args.flatten: _*)

您怎么看?

答案 1 :(得分:-1)

这就是现在实施的方式:

  /**
   * This private helper method transforms a content filter string into an sql expression
   * for searching within movies, owners and kinds and tags.
   * @author Samuel Lörtscher
   */
  private def contentFilterToSql(value: String) = {
    // trim and clean and the parametric value from any possible anomalies
    // (those include strange spacing and non closed quotes)
    val cleaned = value.trim match {
      case trimmed if trimmed.count(_ == '"') % 2 != 0 =>
        if (trimmed.last == '"') trimmed.dropRight(1).trim
        else trimmed + '"'
      case trimmed =>
        trimmed
    };

    // transform the cleaned value into a list of expressions
    // (words between quotes are considered being one expression)
    // empty expressions between quotes are being removed
    // expressions will contain no quotes as they are being stripped during evaluation -
    // thus counter measures for sql injection should be obsolete
    // (we put an empty space at the end because it makes the lexer algorithm much
    // more efficient as it will not need to check for end of file in every iteration)
    val expressions = (cleaned + " ").foldLeft((List[String](), "", false)) { case ((list, expr, quoted), char) =>
      // perform the lexer operation for the current character
      if (char == ' ' && !quoted) (expr :: list, "", false)
      else if (char == '"') (expr :: list, "", !quoted)
      else (list, expr + char, quoted)
    }._1.filter(_.nonEmpty).map(_.trim)

    // finally transform the expression
    // list into a variable length sql condition statement
    expressions.map { expr =>
      s"""
        (concat(Movies.Caption, " ", Movies.Description, " ", Movies.Kind, " ", Profiles.Nickname, " ",
        (select coalesce(group_concat(Tags.Name), "")
        from Tags join TagLinks using (TagID) where TagLinks.MovieID = Movies.MovieID)) like "%$expr%")
      """
    }.mkString(" and ")
  }

由于搜索表达式的数量是可变的,我不能在这里使用Anorm参数。 : - /

我现在找到了一个简单的解决方案,但我很高兴被迫应用这些糟糕的黑客攻击。 由于放置%s字符序列似乎会触发错误,我一直在寻找提交相同语义结果的可能性而不直接传递此字符序列。我最终用替换像“%$ expr%”,如concat(“%”,“$ expr%”)。由于在“喜欢”之前由MySql Server引擎评估concat,因此他将把原始模式重新组合在一起,然后通过“like”处理它 - 并且没有序列%s通过anorm,jdbc数据处理器传输。

// finally transform the expression
// list into a variable length sql condition statement
// (freaking concat("%", "$expr%")) is required due to a freaking bug in either anorm or JDBC
// which results into a crash when %s is anyway submitted)
expressions.map { expr =>
  s"""
    (concat(Movies.Caption, " ", Movies.Description, " ", Movies.Kind, " ", Profiles.Nickname, " ",
    (select coalesce(group_concat(Tags.Name), "")
    from Tags join TagLinks using (TagID) where TagLinks.MovieID = Movies.MovieID)) like concat("%", "$expr%"))
  """
}.mkString(" and ")