import play.api.mvc._
import play.api.libs.ws._
import scala.concurrent.ExecutionContext
import javax.inject.Inject
class ApiController @Inject()(ws: WSClient, cc: ControllerComponents)
(implicit ec: ExecutionContext) extends AbstractController(cc) {
def fetchData = Action.async { implicit request =>
// Vulnerable: User controls URL
val url = request.getQueryString("url").getOrElse("")
// DANGEROUS: Can access internal services
ws.url(url).get().map { response =>
Ok(response.body)
}
}
def proxyRequest = Action.async { implicit request =>
// Vulnerable: POST proxy without validation
val targetUrl = request.body.asFormUrlEncoded
.flatMap(_.get("target_url"))
.flatMap(_.headOption)
.getOrElse("")
// Can be used to attack internal services
ws.url(targetUrl)
.post(request.body.asJson.getOrElse(""))
.map { response =>
Ok(response.json)
}
}
def webhook = Action.async { implicit request =>
// Vulnerable: Webhook URL from user input
val webhookUrl = (request.body.asJson.get \ "webhook_url").as[String]
val data = (request.body.asJson.get \ "data").as[String]
// No validation - can target internal services
ws.url(webhookUrl)
.withHttpHeaders("Content-Type" -> "application/json")
.post(data)
.map { _ =>
Ok("Webhook sent")
}
}
}
import play.api.mvc._
import play.api.libs.ws._
import scala.concurrent.{ExecutionContext, Future}
import java.net.{InetAddress, URI}
import javax.inject.Inject
import scala.util.{Try, Success, Failure}
class ApiController @Inject()(ws: WSClient, cc: ControllerComponents)
(implicit ec: ExecutionContext) extends AbstractController(cc) {
// Allowlist of permitted external domains
private val allowedHosts = Set(
"api.trusted.com",
"secure.partner.com",
"webhook.external.com"
)
// Blocked internal network ranges
private val blockedNetworks = Seq(
"127.0.0.0/8", // Loopback
"10.0.0.0/8", // Private Class A
"172.16.0.0/12", // Private Class B
"192.168.0.0/16", // Private Class C
"169.254.0.0/16", // Link-local
"::1/128", // IPv6 loopback
"fc00::/7" // IPv6 private
)
def validateUrl(url: String): Either[String, URI] = {
if (url.isEmpty) {
return Left("URL cannot be empty")
}
Try(new URI(url)) match {
case Success(uri) =>
// Check scheme
if (!Set("http", "https").contains(uri.getScheme)) {
return Left("Only HTTP and HTTPS schemes are allowed")
}
// Require HTTPS for external calls
if (uri.getScheme != "https") {
return Left("Only HTTPS URLs are allowed")
}
// Check host
val host = uri.getHost
if (host == null || host.isEmpty) {
return Left("Invalid host")
}
// Check against allowlist
if (!allowedHosts.contains(host)) {
return Left(s"Host $host not in allowlist")
}
// Check for IP addresses (should use domain names)
Try(InetAddress.getByName(host)) match {
case Success(addr) =>
val ip = addr.getHostAddress
// Block internal IP ranges
if (isInternalIP(ip)) {
return Left("Access to internal networks is blocked")
}
case Failure(_) =>
return Left("Invalid host address")
}
Right(uri)
case Failure(_) =>
Left("Malformed URL")
}
}
private def isInternalIP(ip: String): Boolean = {
// Simplified check - in production, use proper CIDR matching
ip.startsWith("127.") ||
ip.startsWith("10.") ||
ip.startsWith("192.168.") ||
(ip.startsWith("172.") && {
val octets = ip.split(".")
octets.length >= 2 && {
val second = Try(octets(1).toInt).getOrElse(0)
second >= 16 && second <= 31
}
}) ||
ip.startsWith("169.254.") ||
ip == "::1"
}
def fetchData = Action.async { implicit request =>
val url = request.getQueryString("url").getOrElse("")
validateUrl(url) match {
case Right(validUri) =>
// Secure: Validated URL with timeout
ws.url(validUri.toString)
.withRequestTimeout(10000) // 10 second timeout
.get()
.map { response =>
if (response.status == 200) {
// Limit response size
val limitedBody = response.body.take(1000000) // 1MB limit
Ok(limitedBody)
} else {
BadRequest(s"External service returned ${response.status}")
}
}
.recover {
case _: java.util.concurrent.TimeoutException =>
RequestTimeout("Request timeout")
case _ =>
InternalServerError("External request failed")
}
case Left(error) =>
Future.successful(BadRequest(s"Invalid URL: $error"))
}
}
def proxyRequest = Action.async { implicit request =>
// Secure: Don't allow arbitrary proxying
Future.successful(Forbidden("Proxy functionality disabled for security"))
}
def webhook = Action.async { implicit request =>
request.body.asJson match {
case Some(json) =>
val webhookUrlOpt = (json \ "webhook_url").asOpt[String]
val dataOpt = (json \ "data").asOpt[String]
(webhookUrlOpt, dataOpt) match {
case (Some(webhookUrl), Some(data)) =>
validateUrl(webhookUrl) match {
case Right(validUri) =>
// Secure: Validated webhook with limits
if (data.length > 10000) {
Future.successful(BadRequest("Data payload too large"))
} else {
ws.url(validUri.toString)
.withHttpHeaders(
"Content-Type" -> "application/json",
"User-Agent" -> "SecureApp/1.0"
)
.withRequestTimeout(15000) // 15 second timeout
.post(data)
.map { response =>
if (response.status >= 200 && response.status < 300) {
Ok("Webhook delivered successfully")
} else {
BadRequest(s"Webhook failed with status ${response.status}")
}
}
.recover {
case _: java.util.concurrent.TimeoutException =>
RequestTimeout("Webhook timeout")
case _ =>
InternalServerError("Webhook delivery failed")
}
}
case Left(error) =>
Future.successful(BadRequest(s"Invalid webhook URL: $error"))
}
case _ =>
Future.successful(BadRequest("Missing webhook_url or data"))
}
case None =>
Future.successful(BadRequest("Invalid JSON"))
}
}
// Secure alternative: Predefined service calls
def callTrustedService(serviceId: String) = Action.async { implicit request =>
val trustedServices = Map(
"weather" -> "https://api.trusted.com/weather",
"news" -> "https://api.trusted.com/news",
"status" -> "https://api.trusted.com/status"
)
trustedServices.get(serviceId) match {
case Some(serviceUrl) =>
ws.url(serviceUrl)
.withRequestTimeout(10000)
.get()
.map { response =>
Ok(response.json)
}
.recover {
case _ => InternalServerError("Service call failed")
}
case None =>
Future.successful(BadRequest("Unknown service ID"))
}
}
}