├── public ├── stylesheets │ └── main.css ├── images │ └── favicon.png └── javascripts │ └── jquery-1.9.0.min.js ├── .github ├── mergify.yml ├── dependabot.yml ├── workflows │ ├── dependency-graph.yml │ └── build-test.yml └── scala-steward.conf ├── project ├── build.properties └── plugins.sbt ├── conf ├── messages ├── application.conf ├── logback.xml └── routes ├── .gitignore ├── app ├── views │ ├── main.scala.html │ └── index.scala.html └── controllers │ └── HomeController.scala ├── README.md └── LICENSE /public/stylesheets/main.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/mergify.yml: -------------------------------------------------------------------------------- 1 | extends: .github 2 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.11.7 2 | -------------------------------------------------------------------------------- /public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playframework/play-webgoat/HEAD/public/images/favicon.png -------------------------------------------------------------------------------- /conf/messages: -------------------------------------------------------------------------------- 1 | constraints.invaliduser=You should not type spaces into this field. 2 | 3 | error.invaliduser=The name "{0}" contains spaces! -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | scalacOptions ++= Seq( 2 | "-feature", "-unchecked", "-deprecation", 3 | "-Xlint:-unused", "-Xfatal-warnings") 4 | 5 | addSbtPlugin("org.playframework" % "sbt-plugin" % "3.0.9") 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /*.fpr 2 | /*.log 3 | logs 4 | target/ 5 | tmp 6 | .history 7 | dist 8 | /.idea 9 | /*.iml 10 | /out 11 | /.idea_modules 12 | /.classpath 13 | /.project 14 | /RUNNING_PID 15 | /.settings 16 | /.bsp 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | commit-message: 8 | prefix: "[3.0.x] " 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: "weekly" 13 | target-branch: "2.9.x" 14 | commit-message: 15 | prefix: "[2.9.x] " 16 | -------------------------------------------------------------------------------- /app/views/main.scala.html: -------------------------------------------------------------------------------- 1 | @(title: String)(content: Html) 2 | 3 | 4 | 5 | 6 | 7 | @title 8 | 9 | 10 | 11 | 12 | 13 | @content 14 | 15 | 16 | -------------------------------------------------------------------------------- /conf/application.conf: -------------------------------------------------------------------------------- 1 | # This is the main configuration file for the application. 2 | # ~~~~~ 3 | 4 | # Secret key 5 | # ~~~~~ 6 | # The secret key is used to secure cryptographics functions. 7 | # If you deploy your application to several instances be sure to use the same key! 8 | # https://www.playframework.com/documentation/2.6.x/ApplicationSecret 9 | play.http.secret.key="changeme" 10 | 11 | # Must disable all of the default filters, because otherwise CSRF, AllowedHostsFilter and SecurityHeaderFilter 12 | # will stop a bunch of attacks 13 | # https://www.playframework.com/documentation/2.6.x/Filters 14 | play.filters.enabled=[] 15 | -------------------------------------------------------------------------------- /app/views/index.scala.html: -------------------------------------------------------------------------------- 1 | @(form: Form[controllers.FormData.UserData])(implicit messages: Messages) 2 | 3 | @main("Welcome to Play") { 4 | 5 | @helper.form(routes.HomeController.attackerConstraintForm) { 6 | @if(form.hasGlobalErrors) { 7 | 12 | } 13 | 14 | @* render name value and error information *@ 15 | @helper.inputText(form("name")) 16 | @helper.inputText(form("age")) 17 | 18 | 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/dependency-graph.yml: -------------------------------------------------------------------------------- 1 | name: Dependency Graph 2 | on: 3 | push: 4 | branches: 5 | - 3.0.x 6 | 7 | concurrency: 8 | # Only run once for latest commit per ref and cancel other (previous) runs. 9 | group: dependency-graph-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | permissions: 13 | contents: write # this permission is needed to submit the dependency graph 14 | 15 | jobs: 16 | dependency-graph: 17 | name: Submit dependencies to GitHub 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v6 21 | with: 22 | fetch-depth: 0 23 | ref: ${{ inputs.ref }} 24 | - uses: sbt/setup-sbt@v1 25 | - uses: scalacenter/sbt-dependency-submission@v3 26 | -------------------------------------------------------------------------------- /.github/workflows/build-test.yml: -------------------------------------------------------------------------------- 1 | name: Check 2 | 3 | on: 4 | pull_request: 5 | 6 | push: 7 | branches: 8 | - 3.0.x # Check branch after merge 9 | 10 | concurrency: 11 | # Only run once for latest commit per ref and cancel other (previous) runs. 12 | group: ci-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | tests: 17 | name: Tests 18 | uses: playframework/.github/.github/workflows/cmd.yml@v4 19 | with: 20 | java: 21, 17, 11 21 | scala: 2.13.x, 3.x 22 | cmd: sbt ++$MATRIX_SCALA test 23 | 24 | finish: 25 | name: Finish 26 | if: github.event_name == 'pull_request' 27 | needs: # Should be last 28 | - "tests" 29 | uses: playframework/.github/.github/workflows/rtm.yml@v4 30 | -------------------------------------------------------------------------------- /conf/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | %highlight(%-5level) %logger{15} - %message%n%xException{10} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /.github/scala-steward.conf: -------------------------------------------------------------------------------- 1 | commits.message = "[3.0.x] ${artifactName} ${nextVersion} (was ${currentVersion})" 2 | 3 | pullRequests.grouping = [ 4 | { name = "patches", "title" = "[3.0.x] Patch updates", "filter" = [{"version" = "patch"}] } 5 | ] 6 | 7 | updates.pin = [ 8 | { groupId = "org.playframework", artifactId = "play-ahc-ws", version = "3.0." }, 9 | { groupId = "org.playframework", artifactId = "play-pekko-http-server", version = "3.0." }, 10 | { groupId = "org.playframework", artifactId = "play-filters-helpers", version = "3.0." }, 11 | { groupId = "org.playframework", artifactId = "play-guice", version = "3.0." }, 12 | { groupId = "org.playframework", artifactId = "play-logback", version = "3.0." }, 13 | { groupId = "org.playframework", artifactId = "play-server", version = "3.0." }, 14 | { groupId = "org.playframework", artifactId = "play-test", version = "3.0." }, 15 | { groupId = "org.playframework", artifactId = "sbt-plugin", version = "3.0." } 16 | ] 17 | -------------------------------------------------------------------------------- /conf/routes: -------------------------------------------------------------------------------- 1 | # Routes 2 | # This file defines all application routes (Higher priority routes first) 3 | # ~~~~ 4 | 5 | # Home page 6 | GET / controllers.HomeController.index 7 | 8 | 9 | GET /attackerQuery controllers.HomeController.attackerQuery 10 | GET /attackerQuerySimple controllers.HomeController.attackerQuerySimple 11 | GET /attackerQueryPatternMatching controllers.HomeController.attackerQueryPatternMatching 12 | GET /attackerRouteControlledQuery controllers.HomeController.attackerRouteControlledQuery(attacker) 13 | GET /attackerRouteControlledPath/:attacker controllers.HomeController.attackerRouteControlledPath(attacker) 14 | 15 | # These are a little harder to trigger because you have to push a cookie in the request 16 | GET /attackerCookie controllers.HomeController.attackerCookie 17 | GET /attackerFlash controllers.HomeController.attackerFlash 18 | 19 | # Push an additional header in the request for the attack 20 | GET /attackerHeader controllers.HomeController.attackerHeader 21 | 22 | # Form based attacks 23 | POST /attackerFormInput controllers.HomeController.attackerFormInput 24 | 25 | GET /form controllers.HomeController.constraintForm 26 | POST /attackerConstraintForm controllers.HomeController.attackerConstraintForm 27 | 28 | # SSRF based attack 29 | GET /attackerSSRF controllers.HomeController.attackerSSRF 30 | 31 | # Custom body parser attack 32 | GET /attackerCustomBodyParser controllers.HomeController.attackerCustomBodyParser 33 | 34 | # Map static resources from the /public folder to the /assets URL path 35 | GET /assets/*file controllers.Assets.at(path="/public", file) 36 | 37 | 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # play-webgoat 2 | 3 | [![Twitter Follow](https://img.shields.io/twitter/follow/playframework?label=follow&style=flat&logo=twitter&color=brightgreen)](https://twitter.com/playframework) 4 | [![Discord](https://img.shields.io/discord/931647755942776882?logo=discord&logoColor=white)](https://discord.gg/g5s2vtZ4Fa) 5 | [![GitHub Discussions](https://img.shields.io/github/discussions/playframework/playframework?&logo=github&color=brightgreen)](https://github.com/playframework/playframework/discussions) 6 | [![StackOverflow](https://img.shields.io/static/v1?label=stackoverflow&logo=stackoverflow&logoColor=fe7a16&color=brightgreen&message=playframework)](https://stackoverflow.com/tags/playframework) 7 | [![YouTube](https://img.shields.io/youtube/channel/views/UCRp6QDm5SDjbIuisUpxV9cg?label=watch&logo=youtube&style=flat&color=brightgreen&logoColor=ff0000)](https://www.youtube.com/channel/UCRp6QDm5SDjbIuisUpxV9cg) 8 | [![Twitch Status](https://img.shields.io/twitch/status/playframework?logo=twitch&logoColor=white&color=brightgreen&label=live%20stream)](https://www.twitch.tv/playframework) 9 | [![OpenCollective](https://img.shields.io/opencollective/all/playframework?label=financial%20contributors&logo=open-collective)](https://opencollective.com/playframework) 10 | 11 | [![Build Status](https://github.com/playframework/play-webgoat/actions/workflows/build-test.yml/badge.svg)](https://github.com/playframework/play-webgoat/actions/workflows/build-test.yml) 12 | [![Repository size](https://img.shields.io/github/repo-size/playframework/play-webgoat.svg?logo=git)](https://github.com/playframework/play-webgoat) 13 | [![Scala Steward badge](https://img.shields.io/badge/Scala_Steward-helping-blue.svg?style=flat&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAQCAMAAAARSr4IAAAAVFBMVEUAAACHjojlOy5NWlrKzcYRKjGFjIbp293YycuLa3pYY2LSqql4f3pCUFTgSjNodYRmcXUsPD/NTTbjRS+2jomhgnzNc223cGvZS0HaSD0XLjbaSjElhIr+AAAAAXRSTlMAQObYZgAAAHlJREFUCNdNyosOwyAIhWHAQS1Vt7a77/3fcxxdmv0xwmckutAR1nkm4ggbyEcg/wWmlGLDAA3oL50xi6fk5ffZ3E2E3QfZDCcCN2YtbEWZt+Drc6u6rlqv7Uk0LdKqqr5rk2UCRXOk0vmQKGfc94nOJyQjouF9H/wCc9gECEYfONoAAAAASUVORK5CYII=)](https://scala-steward.org) 14 | [![Mergify Status](https://img.shields.io/endpoint.svg?url=https://api.mergify.com/v1/badges/playframework/play-webgoat&style=flat)](https://mergify.com) 15 | 16 | A vulnerable Play application for attackers. 17 | 18 | This application stays clear of the Twirl template engine for the most part, and shows where unvalidated input from the client can be improperly trusted by the application and included in the response. 19 | 20 | ## Running 21 | 22 | ``` 23 | sbt run 24 | ``` 25 | 26 | Then go to http://localhost:9000. 27 | 28 | ## Scala versions 29 | 30 | Cross-building to Scala 2.13 and 3 is supported. 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | CC0 1.0 Universal 2 | 3 | Statement of Purpose 4 | 5 | The laws of most jurisdictions throughout the world automatically confer 6 | exclusive Copyright and Related Rights (defined below) upon the creator and 7 | subsequent owner(s) (each and all, an "owner") of an original work of 8 | authorship and/or a database (each, a "Work"). 9 | 10 | Certain owners wish to permanently relinquish those rights to a Work for the 11 | purpose of contributing to a commons of creative, cultural and scientific 12 | works ("Commons") that the public can reliably and without fear of later 13 | claims of infringement build upon, modify, incorporate in other works, reuse 14 | and redistribute as freely as possible in any form whatsoever and for any 15 | purposes, including without limitation commercial purposes. These owners may 16 | contribute to the Commons to promote the ideal of a free culture and the 17 | further production of creative, cultural and scientific works, or to gain 18 | reputation or greater distribution for their Work in part through the use and 19 | efforts of others. 20 | 21 | For these and/or other purposes and motivations, and without any expectation 22 | of additional consideration or compensation, the person associating CC0 with a 23 | Work (the "Affirmer"), to the extent that he or she is an owner of Copyright 24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work 25 | and publicly distribute the Work under its terms, with knowledge of his or her 26 | Copyright and Related Rights in the Work and the meaning and intended legal 27 | effect of CC0 on those rights. 28 | 29 | 1. Copyright and Related Rights. A Work made available under CC0 may be 30 | protected by copyright and related or neighboring rights ("Copyright and 31 | Related Rights"). Copyright and Related Rights include, but are not limited 32 | to, the following: 33 | 34 | i. the right to reproduce, adapt, distribute, perform, display, communicate, 35 | and translate a Work; 36 | 37 | ii. moral rights retained by the original author(s) and/or performer(s); 38 | 39 | iii. publicity and privacy rights pertaining to a person's image or likeness 40 | depicted in a Work; 41 | 42 | iv. rights protecting against unfair competition in regards to a Work, 43 | subject to the limitations in paragraph 4(a), below; 44 | 45 | v. rights protecting the extraction, dissemination, use and reuse of data in 46 | a Work; 47 | 48 | vi. database rights (such as those arising under Directive 96/9/EC of the 49 | European Parliament and of the Council of 11 March 1996 on the legal 50 | protection of databases, and under any national implementation thereof, 51 | including any amended or successor version of such directive); and 52 | 53 | vii. other similar, equivalent or corresponding rights throughout the world 54 | based on applicable law or treaty, and any national implementations thereof. 55 | 56 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, 57 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and 58 | unconditionally waives, abandons, and surrenders all of Affirmer's Copyright 59 | and Related Rights and associated claims and causes of action, whether now 60 | known or unknown (including existing as well as future claims and causes of 61 | action), in the Work (i) in all territories worldwide, (ii) for the maximum 62 | duration provided by applicable law or treaty (including future time 63 | extensions), (iii) in any current or future medium and for any number of 64 | copies, and (iv) for any purpose whatsoever, including without limitation 65 | commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes 66 | the Waiver for the benefit of each member of the public at large and to the 67 | detriment of Affirmer's heirs and successors, fully intending that such Waiver 68 | shall not be subject to revocation, rescission, cancellation, termination, or 69 | any other legal or equitable action to disrupt the quiet enjoyment of the Work 70 | by the public as contemplated by Affirmer's express Statement of Purpose. 71 | 72 | 3. Public License Fallback. Should any part of the Waiver for any reason be 73 | judged legally invalid or ineffective under applicable law, then the Waiver 74 | shall be preserved to the maximum extent permitted taking into account 75 | Affirmer's express Statement of Purpose. In addition, to the extent the Waiver 76 | is so judged Affirmer hereby grants to each affected person a royalty-free, 77 | non transferable, non sublicensable, non exclusive, irrevocable and 78 | unconditional license to exercise Affirmer's Copyright and Related Rights in 79 | the Work (i) in all territories worldwide, (ii) for the maximum duration 80 | provided by applicable law or treaty (including future time extensions), (iii) 81 | in any current or future medium and for any number of copies, and (iv) for any 82 | purpose whatsoever, including without limitation commercial, advertising or 83 | promotional purposes (the "License"). The License shall be deemed effective as 84 | of the date CC0 was applied by Affirmer to the Work. Should any part of the 85 | License for any reason be judged legally invalid or ineffective under 86 | applicable law, such partial invalidity or ineffectiveness shall not 87 | invalidate the remainder of the License, and in such case Affirmer hereby 88 | affirms that he or she will not (i) exercise any of his or her remaining 89 | Copyright and Related Rights in the Work or (ii) assert any associated claims 90 | and causes of action with respect to the Work, in either case contrary to 91 | Affirmer's express Statement of Purpose. 92 | 93 | 4. Limitations and Disclaimers. 94 | 95 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 96 | surrendered, licensed or otherwise affected by this document. 97 | 98 | b. Affirmer offers the Work as-is and makes no representations or warranties 99 | of any kind concerning the Work, express, implied, statutory or otherwise, 100 | including without limitation warranties of title, merchantability, fitness 101 | for a particular purpose, non infringement, or the absence of latent or 102 | other defects, accuracy, or the present or absence of errors, whether or not 103 | discoverable, all to the greatest extent permissible under applicable law. 104 | 105 | c. Affirmer disclaims responsibility for clearing rights of other persons 106 | that may apply to the Work or any use thereof, including without limitation 107 | any person's Copyright and Related Rights in the Work. Further, Affirmer 108 | disclaims responsibility for obtaining any necessary consents, permissions 109 | or other rights required for any use of the Work. 110 | 111 | d. Affirmer understands and acknowledges that Creative Commons is not a 112 | party to this document and has no duty or obligation with respect to this 113 | CC0 or use of the Work. 114 | 115 | For more information, please see 116 | 117 | -------------------------------------------------------------------------------- /app/controllers/HomeController.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import javax.inject.Inject 4 | 5 | import play.api.libs.streams.Accumulator 6 | import play.api.libs.ws.WSClient 7 | import play.api.mvc._ 8 | import play.twirl.api.Html 9 | 10 | import scala.concurrent.ExecutionContext 11 | import scala.sys.process._ 12 | 13 | /** 14 | * A controller full of vulnerabilities. 15 | */ 16 | class HomeController @Inject()(ws: WSClient, cc: MessagesControllerComponents)(implicit ec: ExecutionContext) extends MessagesAbstractController(cc) { 17 | 18 | def index: Action[AnyContent] = Action { implicit request => 19 | Ok(Html(s""" 20 | 21 | 22 | 36 | 37 | 38 | """)) 39 | } 40 | 41 | /** 42 | * Command injection & XSS directly from directly called query parameter 43 | */ 44 | def attackerQuerySimple: Action[AnyContent] = Action { implicit request => 45 | val address = request.getQueryString("address") 46 | 47 | // [RuleTest] Command Injection 48 | s"ping ${address}".! 49 | 50 | // [RuleTest] Cross-Site Scripting: Reflected 51 | val html = Html(s"Host ${address} pinged") 52 | 53 | Ok(html) as HTML 54 | } 55 | 56 | /** 57 | * Command injection & XSS directly from directly called query parameter 58 | */ 59 | def attackerQueryPatternMatching: Action[AnyContent] = Action { implicit request => 60 | 61 | val addressRE= "(.*):(\\d+)".r 62 | val address = request.cookies.get("address").get.value 63 | 64 | address match { 65 | // [RuleTest] Command Injection 66 | case addressRE(address, port) => s"ping ${address}".! 67 | case _ => 68 | } 69 | // [RuleTest] Cross-Site Scripting: Reflected 70 | Ok(Html(s"Host ${address} pinged")) as HTML 71 | } 72 | 73 | /** 74 | * XSS directly from directly called query parameter 75 | */ 76 | def attackerQuery: Action[AnyContent] = Action { implicit request => 77 | 78 | val result = request.getQueryString("attacker").map { command => 79 | // Render the command directly from query parameter, this is the obvious example 80 | // User thinks this is system controlled and validated, so turns it into HTML 81 | // and unescapes it, resulting in XSS. 82 | Ok(Html(command)) 83 | }.getOrElse(Ok("")) 84 | 85 | result as HTML 86 | } 87 | 88 | /** 89 | * XSS through query string parsed by generated router from conf/routes file. 90 | */ 91 | def attackerRouteControlledQuery(attacker: String): Action[AnyContent] = Action { implicit request => 92 | Ok(Html(attacker)) as HTML 93 | } 94 | 95 | /** 96 | * XSS through path binding parsed by generated router from conf/routes file. 97 | */ 98 | def attackerRouteControlledPath(attacker: String): Action[AnyContent] = Action { implicit request => 99 | Ok(Html(attacker)) as HTML 100 | } 101 | 102 | /** 103 | * XSS through attacker controlled info in cookie 104 | */ 105 | def attackerCookie: Action[AnyContent] = Action { implicit request => 106 | // User cookies have no message authentication by default, so an attacker can pass in a cookie 107 | val result = request.cookies.get("attacker").map { attackerCookie => 108 | // Render the command 109 | Ok(Html(attackerCookie.value)) 110 | }.getOrElse(Ok("")) 111 | 112 | result as HTML 113 | } 114 | 115 | /** 116 | * XSS through attacker controlled header 117 | */ 118 | def attackerHeader: Action[AnyContent] = Action { implicit request => 119 | // Request headers are also unvalidated by default. 120 | // The usual example is pulling the Location header to do an unsafe redirect 121 | val result = request.headers.get("Attacker").map { command => 122 | // Render the command 123 | Ok(command) 124 | }.getOrElse(Ok("")) 125 | 126 | result as HTML 127 | } 128 | 129 | /** 130 | * Unbound redirect through Header 131 | */ 132 | def attackerOpenRedirect: Action[AnyContent] = Action { implicit request => 133 | request.headers.get("Location") match { 134 | case Some(attackerLocation) => 135 | // Also see https://github.com/playframework/playframework/issues/6450 136 | Redirect(attackerLocation) 137 | 138 | case None => 139 | Ok("No location found!") 140 | } 141 | } 142 | 143 | /** 144 | * XSS through URL encoded form input. 145 | */ 146 | def attackerFormInput: Action[AnyContent] = Action { implicit request => 147 | val boundForm = FormData.form.bindFromRequest() 148 | boundForm.fold(badData => BadRequest("Bad form binding"), userData => { 149 | // Render the attacker command as HTML 150 | val command = userData.name 151 | Ok(Html(command)) as HTML 152 | }) 153 | } 154 | 155 | /** 156 | * XSS through attacker controlled flash cookie. 157 | */ 158 | def attackerFlash: Action[AnyContent] = Action { implicit request => 159 | // Flash is usually handled with 160 | // Redirect(routes.HomeController.attackerFlash()).flashing("info" -> "Some text") 161 | // but if the user puts HTML in it and then renders it, 162 | // Flash is not signed by default in 2.5.x, and so will take a user's unvalidated 163 | // flash cookie as input. 164 | 165 | val result = request.flash.get("info").map { command => 166 | // Page displays XSS in the flashing input. 167 | Ok(Html(command)) 168 | }.getOrElse(Ok("")) 169 | 170 | result as HTML 171 | } 172 | 173 | // Render a boring form 174 | def constraintForm: Action[AnyContent] = Action { implicit request => 175 | Ok(views.html.index(FormData.customForm)) 176 | } 177 | 178 | /** 179 | * XSS through custom constraint with user input 180 | */ 181 | def attackerConstraintForm: Action[AnyContent] = Action { implicit request => 182 | 183 | // Bind a form that uses the i18n messages api, but the user input is reflected in the error message 184 | // Play takes a raw string here and escapes everything, but it may be possible to escape this... 185 | val boundForm = FormData.customForm.bindFromRequest() 186 | boundForm.fold(formWithErrors => { 187 | val nameField = formWithErrors("name") 188 | nameField.error.map { error => 189 | val formWithMoreErrors = formWithErrors.withGlobalError(s"""You should not have typed ${error.message}""") 190 | BadRequest(views.html.index(formWithMoreErrors)) 191 | }.getOrElse(BadRequest(views.html.index(formWithErrors))) 192 | }, userData => { 193 | Ok("everything is fine") as HTML 194 | }) 195 | } 196 | 197 | /** 198 | * SSRF attacks done with Play WS 199 | */ 200 | def attackerSSRF: Action[AnyContent] = Action.async { implicit request => 201 | // Play WS does not have a whitelist of valid URLs, so if you're calling it 202 | // directly with user input, you're open to SSRF. The best thing to do is 203 | // to place WS access in a wrapper, i.e. 204 | // https://www.playframework.com/documentation/2.5.x/ScalaTestingWebServiceClients#Testing-a-GitHub-client 205 | 206 | val attackerUrl = request.body.asText.getOrElse("http://google.com") 207 | 208 | ws.url(attackerUrl).get().map { response => 209 | // For bonus points, we can pull things out of the response as well... 210 | Ok(s"I called out to $attackerUrl") 211 | } 212 | } 213 | 214 | /** 215 | * Command injection with custom body parser 216 | */ 217 | def attackerCustomBodyParser: Action[Foo] = Action(bodyParser = BodyParser { (header: RequestHeader) => { 218 | // request header is a request without a body 219 | // http://localhost:9000/attackerCustomBodyParser?address=/etc/passwd 220 | val result = header.getQueryString("filename").map { filename => 221 | // [RuleTest] Command Injection 222 | s"cat ${filename}".!! 223 | }.getOrElse("No filename found!") 224 | 225 | Accumulator.done(Right(Foo(bar = result))) 226 | }}) { implicit request: Request[Foo] => 227 | val foo: Foo = request.body 228 | Ok(foo.bar) 229 | } 230 | 231 | case class Foo(bar: String) 232 | // Full on level 3 HATEOAS REST APIs are particularly dumb about following 233 | // URL links blindly... could use Siren/HAL/JSON-LD to make this more interesting... 234 | // 235 | //import play.api.libs.json._ 236 | //val json: JsValue = request.body.asJson.getOrElse(Json.arr()) 237 | //val attackerUrl: String = (json \ "attacker").get.as[String] 238 | 239 | // Attacks through custom body parsers? 240 | // https://www.playframework.com/documentation/2.5.x/ScalaBodyParsers#Writing-a-custom-body-parser 241 | 242 | // Attacks through custom multipart file upload? 243 | // https://github.com/playframework/play-scala-fileupload-example 244 | 245 | // CSRF attacks? 246 | // Play does not use CSRF tokens or SameSite unless you specifically enable the 247 | // CSRFFilter: 248 | // https://www.playframework.com/documentation/2.5.x/ScalaCsrf 249 | 250 | // Sadly chrome extensions can POST directly: see 251 | // https://www.playframework.com/security/vulnerability/20160304-CsrfBypass 252 | // for details. 253 | 254 | // DNS rebinding attacks still to do (practical without dnsrebinder.net?) 255 | // http://benmmurphy.github.io/blog/2016/07/11/rails-webconsole-dns-rebinding/ 256 | // https://github.com/benmmurphy/rebinder 257 | // https://www.playframework.com/documentation/2.5.x/AllowedHostsFilter 258 | 259 | // Sidejacking attacks not really practical without subdomains 260 | 261 | // Fairly easy to do DoS attacks if you buffer a stream in memory with an unbounded data structure... 262 | } 263 | 264 | object FormData { 265 | import play.api.data.Form 266 | 267 | import play.api.data.Forms._ 268 | 269 | def form = Form( 270 | mapping( 271 | "name" -> text, 272 | "age" -> number 273 | )(UserData.apply)(UserData.unapply) 274 | ) 275 | 276 | def customForm = Form( 277 | mapping( 278 | "name" -> text.verifying(customConstraint), 279 | "age" -> number 280 | )(UserData.apply)(UserData.unapply) 281 | ) 282 | 283 | import play.api.data.validation._ 284 | 285 | // https://playframework.com/documentation/2.5.x/ScalaCustomValidations 286 | val customConstraint: Constraint[String] = Constraint("constraints.invaliduser")({ 287 | attackerInput => 288 | 289 | val errors = attackerInput match { 290 | case s: String if s.contains(" ") => 291 | // Error message contains attacker controlled input text 292 | // Play takes a raw string here and escapes everything, but it may be possible to escape this... 293 | Seq(ValidationError("error.invaliduser", attackerInput)) 294 | 295 | case _ => 296 | Nil 297 | } 298 | 299 | if (errors.isEmpty) { 300 | Valid 301 | } else { 302 | Invalid(errors) 303 | } 304 | }) 305 | 306 | 307 | case class UserData(name: String, age:Int) 308 | object UserData { 309 | def unapply(u: UserData): Option[(String, Int)] = Some((u.name, u.age)) 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /public/javascripts/jquery-1.9.0.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery v1.9.0 | (c) 2005, 2012 jQuery Foundation, Inc. | jquery.org/license */(function(e,t){"use strict";function n(e){var t=e.length,n=st.type(e);return st.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}function r(e){var t=Tt[e]={};return st.each(e.match(lt)||[],function(e,n){t[n]=!0}),t}function i(e,n,r,i){if(st.acceptData(e)){var o,a,s=st.expando,u="string"==typeof n,l=e.nodeType,c=l?st.cache:e,f=l?e[s]:e[s]&&s;if(f&&c[f]&&(i||c[f].data)||!u||r!==t)return f||(l?e[s]=f=K.pop()||st.guid++:f=s),c[f]||(c[f]={},l||(c[f].toJSON=st.noop)),("object"==typeof n||"function"==typeof n)&&(i?c[f]=st.extend(c[f],n):c[f].data=st.extend(c[f].data,n)),o=c[f],i||(o.data||(o.data={}),o=o.data),r!==t&&(o[st.camelCase(n)]=r),u?(a=o[n],null==a&&(a=o[st.camelCase(n)])):a=o,a}}function o(e,t,n){if(st.acceptData(e)){var r,i,o,a=e.nodeType,u=a?st.cache:e,l=a?e[st.expando]:st.expando;if(u[l]){if(t&&(r=n?u[l]:u[l].data)){st.isArray(t)?t=t.concat(st.map(t,st.camelCase)):t in r?t=[t]:(t=st.camelCase(t),t=t in r?[t]:t.split(" "));for(i=0,o=t.length;o>i;i++)delete r[t[i]];if(!(n?s:st.isEmptyObject)(r))return}(n||(delete u[l].data,s(u[l])))&&(a?st.cleanData([e],!0):st.support.deleteExpando||u!=u.window?delete u[l]:u[l]=null)}}}function a(e,n,r){if(r===t&&1===e.nodeType){var i="data-"+n.replace(Nt,"-$1").toLowerCase();if(r=e.getAttribute(i),"string"==typeof r){try{r="true"===r?!0:"false"===r?!1:"null"===r?null:+r+""===r?+r:wt.test(r)?st.parseJSON(r):r}catch(o){}st.data(e,n,r)}else r=t}return r}function s(e){var t;for(t in e)if(("data"!==t||!st.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}function u(){return!0}function l(){return!1}function c(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}function f(e,t,n){if(t=t||0,st.isFunction(t))return st.grep(e,function(e,r){var i=!!t.call(e,r,e);return i===n});if(t.nodeType)return st.grep(e,function(e){return e===t===n});if("string"==typeof t){var r=st.grep(e,function(e){return 1===e.nodeType});if(Wt.test(t))return st.filter(t,r,!n);t=st.filter(t,r)}return st.grep(e,function(e){return st.inArray(e,t)>=0===n})}function p(e){var t=zt.split("|"),n=e.createDocumentFragment();if(n.createElement)for(;t.length;)n.createElement(t.pop());return n}function d(e,t){return e.getElementsByTagName(t)[0]||e.appendChild(e.ownerDocument.createElement(t))}function h(e){var t=e.getAttributeNode("type");return e.type=(t&&t.specified)+"/"+e.type,e}function g(e){var t=nn.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function m(e,t){for(var n,r=0;null!=(n=e[r]);r++)st._data(n,"globalEval",!t||st._data(t[r],"globalEval"))}function y(e,t){if(1===t.nodeType&&st.hasData(e)){var n,r,i,o=st._data(e),a=st._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;i>r;r++)st.event.add(t,n,s[n][r])}a.data&&(a.data=st.extend({},a.data))}}function v(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!st.support.noCloneEvent&&t[st.expando]){r=st._data(t);for(i in r.events)st.removeEvent(t,i,r.handle);t.removeAttribute(st.expando)}"script"===n&&t.text!==e.text?(h(t).text=e.text,g(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),st.support.html5Clone&&e.innerHTML&&!st.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Zt.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}function b(e,n){var r,i,o=0,a=e.getElementsByTagName!==t?e.getElementsByTagName(n||"*"):e.querySelectorAll!==t?e.querySelectorAll(n||"*"):t;if(!a)for(a=[],r=e.childNodes||e;null!=(i=r[o]);o++)!n||st.nodeName(i,n)?a.push(i):st.merge(a,b(i,n));return n===t||n&&st.nodeName(e,n)?st.merge([e],a):a}function x(e){Zt.test(e.type)&&(e.defaultChecked=e.checked)}function T(e,t){if(t in e)return t;for(var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=Nn.length;i--;)if(t=Nn[i]+n,t in e)return t;return r}function w(e,t){return e=t||e,"none"===st.css(e,"display")||!st.contains(e.ownerDocument,e)}function N(e,t){for(var n,r=[],i=0,o=e.length;o>i;i++)n=e[i],n.style&&(r[i]=st._data(n,"olddisplay"),t?(r[i]||"none"!==n.style.display||(n.style.display=""),""===n.style.display&&w(n)&&(r[i]=st._data(n,"olddisplay",S(n.nodeName)))):r[i]||w(n)||st._data(n,"olddisplay",st.css(n,"display")));for(i=0;o>i;i++)n=e[i],n.style&&(t&&"none"!==n.style.display&&""!==n.style.display||(n.style.display=t?r[i]||"":"none"));return e}function C(e,t,n){var r=mn.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function k(e,t,n,r,i){for(var o=n===(r?"border":"content")?4:"width"===t?1:0,a=0;4>o;o+=2)"margin"===n&&(a+=st.css(e,n+wn[o],!0,i)),r?("content"===n&&(a-=st.css(e,"padding"+wn[o],!0,i)),"margin"!==n&&(a-=st.css(e,"border"+wn[o]+"Width",!0,i))):(a+=st.css(e,"padding"+wn[o],!0,i),"padding"!==n&&(a+=st.css(e,"border"+wn[o]+"Width",!0,i)));return a}function E(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=ln(e),a=st.support.boxSizing&&"border-box"===st.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=un(e,t,o),(0>i||null==i)&&(i=e.style[t]),yn.test(i))return i;r=a&&(st.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+k(e,t,n||(a?"border":"content"),r,o)+"px"}function S(e){var t=V,n=bn[e];return n||(n=A(e,t),"none"!==n&&n||(cn=(cn||st("