Skip to content

Commit

Permalink
Merge pull request #123 from playframework/plugin-1.1.0-RC1
Browse files Browse the repository at this point in the history
Fortify: move to Play 2.9 and add Scala 3
  • Loading branch information
SethTisue authored Dec 6, 2023
2 parents 3a5d27a + b4ddca6 commit e7a1f29
Show file tree
Hide file tree
Showing 11 changed files with 323 additions and 303 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/fortify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ jobs:
strategy:
fail-fast: false
matrix:
java: [8, 11, 17]
scala: [2.12.x, 2.13.x]
java: [11, 17, 21]
scala: [2.13.x, 3.x]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: coursier/cache-action@v6
- uses: actions/setup-java@v3
- uses: actions/setup-java@v4
with:
distribution: adopt
distribution: temurin
java-version: ${{matrix.java}}

- uses: actions/cache@v3
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ dist
/.project
/RUNNING_PID
/.settings
/.bsp
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@ Then go to http://localhost:9000.

## Scala versions

Cross-building to Scala 2.12 and 2.13 is supported.
Cross-building to Scala 2.13 and 3 is supported.
40 changes: 22 additions & 18 deletions app/controllers/HomeController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import scala.sys.process._
*/
class HomeController @Inject()(ws: WSClient, cc: MessagesControllerComponents)(implicit ec: ExecutionContext) extends MessagesAbstractController(cc) {

def index = Action { implicit request =>
def index: Action[AnyContent] = Action { implicit request =>
Ok(Html(s"""
<html>
<body>
Expand All @@ -41,7 +41,7 @@ class HomeController @Inject()(ws: WSClient, cc: MessagesControllerComponents)(i
/**
* Command injection & XSS directly from directly called query parameter
*/
def attackerQuerySimple = Action { implicit request =>
def attackerQuerySimple: Action[AnyContent] = Action { implicit request =>
val address = request.getQueryString("address")

// [RuleTest] Command Injection
Expand All @@ -56,14 +56,15 @@ class HomeController @Inject()(ws: WSClient, cc: MessagesControllerComponents)(i
/**
* Command injection & XSS directly from directly called query parameter
*/
def attackerQueryPatternMatching = Action { implicit request =>
def attackerQueryPatternMatching: Action[AnyContent] = Action { implicit request =>

val addressRE= "(.*):(\\d+)".r
val address = request.cookies.get("address").get.value

address match {
// [RuleTest] Command Injection
// [RuleTest] Command Injection
case addressRE(address, port) => s"ping ${address}".!
case _ =>
}
// [RuleTest] Cross-Site Scripting: Reflected
Ok(Html(s"Host ${address} pinged")) as HTML
Expand All @@ -72,7 +73,7 @@ class HomeController @Inject()(ws: WSClient, cc: MessagesControllerComponents)(i
/**
* XSS directly from directly called query parameter
*/
def attackerQuery = Action { implicit request =>
def attackerQuery: Action[AnyContent] = Action { implicit request =>

val result = request.getQueryString("attacker").map { command =>
// Render the command directly from query parameter, this is the obvious example
Expand All @@ -87,21 +88,21 @@ class HomeController @Inject()(ws: WSClient, cc: MessagesControllerComponents)(i
/**
* XSS through query string parsed by generated router from conf/routes file.
*/
def attackerRouteControlledQuery(attacker: String) = Action { implicit request =>
def attackerRouteControlledQuery(attacker: String): Action[AnyContent] = Action { implicit request =>
Ok(Html(attacker)) as HTML
}

/**
* XSS through path binding parsed by generated router from conf/routes file.
*/
def attackerRouteControlledPath(attacker: String) = Action { implicit request =>
def attackerRouteControlledPath(attacker: String): Action[AnyContent] = Action { implicit request =>
Ok(Html(attacker)) as HTML
}

/**
* XSS through attacker controlled info in cookie
*/
def attackerCookie = Action { implicit request =>
def attackerCookie: Action[AnyContent] = Action { implicit request =>
// User cookies have no message authentication by default, so an attacker can pass in a cookie
val result = request.cookies.get("attacker").map { attackerCookie =>
// Render the command
Expand All @@ -114,7 +115,7 @@ class HomeController @Inject()(ws: WSClient, cc: MessagesControllerComponents)(i
/**
* XSS through attacker controlled header
*/
def attackerHeader = Action { implicit request =>
def attackerHeader: Action[AnyContent] = Action { implicit request =>
// Request headers are also unvalidated by default.
// The usual example is pulling the Location header to do an unsafe redirect
val result = request.headers.get("Attacker").map { command =>
Expand All @@ -128,7 +129,7 @@ class HomeController @Inject()(ws: WSClient, cc: MessagesControllerComponents)(i
/**
* Unbound redirect through Header
*/
def attackerOpenRedirect = Action { implicit request =>
def attackerOpenRedirect: Action[AnyContent] = Action { implicit request =>
request.headers.get("Location") match {
case Some(attackerLocation) =>
// Also see https://github.com/playframework/playframework/issues/6450
Expand All @@ -142,7 +143,7 @@ class HomeController @Inject()(ws: WSClient, cc: MessagesControllerComponents)(i
/**
* XSS through URL encoded form input.
*/
def attackerFormInput = Action { implicit request =>
def attackerFormInput: Action[AnyContent] = Action { implicit request =>
val boundForm = FormData.form.bindFromRequest()
boundForm.fold(badData => BadRequest("Bad form binding"), userData => {
// Render the attacker command as HTML
Expand All @@ -154,7 +155,7 @@ class HomeController @Inject()(ws: WSClient, cc: MessagesControllerComponents)(i
/**
* XSS through attacker controlled flash cookie.
*/
def attackerFlash = Action { implicit request =>
def attackerFlash: Action[AnyContent] = Action { implicit request =>
// Flash is usually handled with
// Redirect(routes.HomeController.attackerFlash()).flashing("info" -> "Some text")
// but if the user puts HTML in it and then renders it,
Expand All @@ -170,14 +171,14 @@ class HomeController @Inject()(ws: WSClient, cc: MessagesControllerComponents)(i
}

// Render a boring form
def constraintForm = Action { implicit request =>
def constraintForm: Action[AnyContent] = Action { implicit request =>
Ok(views.html.index(FormData.customForm))
}

/**
* XSS through custom constraint with user input
*/
def attackerConstraintForm = Action { implicit request =>
def attackerConstraintForm: Action[AnyContent] = Action { implicit request =>

// Bind a form that uses the i18n messages api, but the user input is reflected in the error message
// Play takes a raw string here and escapes everything, but it may be possible to escape this...
Expand All @@ -196,7 +197,7 @@ class HomeController @Inject()(ws: WSClient, cc: MessagesControllerComponents)(i
/**
* XSS involving Twirl template
*/
def twirlXSS = Action { implicit request =>
def twirlXSS = Action { implicit request: MessagesRequest[AnyContent] =>
request.getQueryString("xss").map { payload =>
Ok(views.html.xss(payload))
}.getOrElse(Ok("Missing xss param"))
Expand All @@ -205,7 +206,7 @@ class HomeController @Inject()(ws: WSClient, cc: MessagesControllerComponents)(i
/**
* SSRF attacks done with Play WS
*/
def attackerSSRF = Action.async { implicit request =>
def attackerSSRF: Action[AnyContent] = Action.async { implicit request =>
// Play WS does not have a whitelist of valid URLs, so if you're calling it
// directly with user input, you're open to SSRF. The best thing to do is
// to place WS access in a wrapper, i.e.
Expand All @@ -222,7 +223,7 @@ class HomeController @Inject()(ws: WSClient, cc: MessagesControllerComponents)(i
/**
* Command injection with custom body parser
*/
def attackerCustomBodyParser = Action(bodyParser = BodyParser { header: RequestHeader =>
def attackerCustomBodyParser: Action[Foo] = Action(bodyParser = BodyParser { (header: RequestHeader) => {
// request header is a request without a body
// http://localhost:9000/attackerCustomBodyParser?address=/etc/passwd
val result = header.getQueryString("filename").map { filename =>
Expand All @@ -231,7 +232,7 @@ class HomeController @Inject()(ws: WSClient, cc: MessagesControllerComponents)(i
}.getOrElse("No filename found!")

Accumulator.done(Right(Foo(bar = result)))
}) { implicit request: Request[Foo] =>
}}) { implicit request: Request[Foo] =>
val foo: Foo = request.body
Ok(foo.bar)
}
Expand Down Expand Up @@ -313,4 +314,7 @@ object FormData {


case class UserData(name: String, age:Int)
object UserData {
def unapply(u: UserData): Option[(String, Int)] = Some((u.name, u.age))
}
}
11 changes: 7 additions & 4 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ lazy val `play-webgoat` = (project in file(".")).enablePlugins(PlayScala)
name := "play-webgoat"
version := "1.0"

crossScalaVersions := Seq("2.13.12", "2.12.18")
crossScalaVersions := Seq("2.13.12", "3.3.1")
scalaVersion := crossScalaVersions.value.head // tc-skip

libraryDependencies ++= Seq(guice, ws)
scalacOptions ++= Seq(
"-feature", "-unchecked", "-deprecation", "-Xfatal-warnings",
// "unused" is too fragile w/ Twirl, routes file
"-Xlint:-unused"
// "-unchecked", "-deprecation" // Set by Play already
"-feature", "-Werror",
)
scalacOptions ++= (CrossVersion.partialVersion(scalaVersion.value) match {
case Some((2, _)) => Seq("-Xlint:-unused,_")
case _ => Seq()
})
17 changes: 12 additions & 5 deletions conf/logback.xml
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
<?xml version="1.0" encoding="UTF-8" ?>

<!--
~ Copyright (C) 2009-2017 Lightbend Inc. <https://www.lightbend.com>
-->
Copyright (C) from 2022 The Play Framework Contributors <https://github.com/playframework>, 2011-2021 Lightbend Inc. <https://www.lightbend.com>
-->

<!DOCTYPE configuration>

<!-- The default logback configuration that Play uses in dev mode if no other configuration is provided -->
<configuration>
<import class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"/>
<import class="ch.qos.logback.core.ConsoleAppender"/>

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<appender name="STDOUT" class="ConsoleAppender">
<encoder class="PatternLayoutEncoder">
<pattern>%highlight(%-5level) %logger{15} - %message%n%xException{10}</pattern>
</encoder>
</appender>

<root level="INFO">
<appender-ref ref="STDOUT" />
<appender-ref ref="STDOUT"/>
</root>

</configuration>
2 changes: 1 addition & 1 deletion fortify.sbt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// enable the plugin
addCompilerPlugin(
"com.lightbend" %% "scala-fortify" % "1.0.25"
"com.lightbend" %% "scala-fortify" % "1.1.0-RC1"
cross CrossVersion.patch)

// configure the plugin
Expand Down
6 changes: 1 addition & 5 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,4 @@ scalacOptions ++= Seq(
"-feature", "-unchecked", "-deprecation",
"-Xlint:-unused", "-Xfatal-warnings")

ThisBuild / libraryDependencySchemes ++= Seq(
"org.scala-lang.modules" %% "scala-xml" % VersionScheme.Always
)

addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.19")
addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.9.0")
Loading

0 comments on commit e7a1f29

Please sign in to comment.