Skip to content
This repository was archived by the owner on Aug 28, 2024. It is now read-only.

Commit 502ffa8

Browse files
author
Include Security
committed
v1.0
0 parents  commit 502ffa8

File tree

9 files changed

+728
-0
lines changed

9 files changed

+728
-0
lines changed

LICENSE

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2016 Include Security
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy of
6+
this software and associated documentation files (the "Software"), to deal in
7+
the Software without restriction, including without limitation the rights to
8+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9+
the Software, and to permit persons to whom the Software is furnished to do so,
10+
subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# SafeURL for Scala
2+
### Originally Ported by [@saelo](https://github.com/saelo)
3+
4+
## Overview
5+
SafeURL is a library that aids developers in protecting against a class of vulnerabilities known as [Server Side Request Forgery](http://www.acunetix.com/blog/articles/server-side-request-forgery-vulnerability/). It does this by validating each part of the URL against a configurable white or black list before making an HTTP request. S
6+
afeURL is open-source and licensed under MIT.
7+
8+
## Installation
9+
Clone this repository and import it into your project.
10+
11+
## Implementation
12+
SafeURL replaces the Java methods in the [URLConnection](https://docs.oracle.com/javase/7/docs/api/java/net/URLConnection.html) class that are normally used to make HTTP requests in Scala.
13+
14+
```scala
15+
try {
16+
//User controlled input
17+
val url = url_
18+
//Execute using SafeURL
19+
val resp = SafeURL.fetch(url)
20+
val r = Await.result(resp, 500 millis)
21+
} catch {
22+
//URL wasnt safe
23+
}
24+
```
25+
## Configuration
26+
Options such as white and black lists can be modified. For example:
27+
28+
```scala
29+
//Deny requests to specific IPs
30+
SafeURL.defaultConfiguration.lists.ip.blacklist ::= "12.34.0.0/16"
31+
//Deny requests to specific domains
32+
SafeURL.defaultConfiguration.lists.domain.blacklist ::= "example.com"
33+
```

examples/BasicExample.scala

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import scala.concurrent.ExecutionContext.Implicits.global
2+
import scala.concurrent.Await
3+
import scala.concurrent.duration._
4+
5+
import com.includesecurity.safeurl._
6+
7+
object BasicExample {
8+
def main(Args: Array[String]) {
9+
//
10+
// Asyncronous example
11+
//
12+
var futureResponse = SafeURL.fetch("http://icanhazip.com")
13+
futureResponse onSuccess {
14+
case response => println(response.asString.trim)
15+
}
16+
17+
//
18+
// Syncronous example
19+
//
20+
futureResponse = SafeURL.fetch("http://icanhazip.com")
21+
val response = Await.result(futureResponse, 5000 millis)
22+
println(response.asString.trim)
23+
24+
25+
//
26+
// Dealing with errors
27+
//
28+
futureResponse = SafeURL.fetch("file:///etc/passwd")
29+
futureResponse onSuccess {
30+
case response => println(response.asString)
31+
}
32+
futureResponse onFailure {
33+
case error => println(error.toString)
34+
}
35+
36+
futureResponse = SafeURL.fetch("http://192.168.1.1/secret")
37+
try {
38+
val response = Await.result(futureResponse, 5000 millis)
39+
} catch {
40+
case DisallowedURLException(URLPart.IP, ip, _) => println("Sorry, you can't access that IP (" + ip + ")")
41+
case e: DisallowedURLException => println("Access denied")
42+
}
43+
44+
// wait for asyncronous tasks to finish
45+
Thread.sleep(1000)
46+
}
47+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import scala.concurrent.ExecutionContext.Implicits.global
2+
import scala.concurrent.Await
3+
import scala.concurrent.duration._
4+
5+
import com.includesecurity.safeurl._
6+
7+
object ConfigurationExample {
8+
def main(args: Array[String]) {
9+
//
10+
// Modify the default configuration
11+
//
12+
SafeURL.defaultConfiguration.lists.ip.blacklist ::= "12.34.0.0/16"
13+
14+
try {
15+
SafeURL.validate("http://12.34.43.21")
16+
} catch {
17+
case e: DisallowedURLException => println(e.toString)
18+
}
19+
20+
21+
//
22+
// Use a custom configuration for a single request
23+
//
24+
var config = new Configuration
25+
config.lists.protocol.whitelist ::= "ftp"
26+
27+
var futureResponse = SafeURL.fetch("ftp://cdimage.debian.org/debian-cd/7.6.0/amd64/iso-cd/SHA256SUMS", config)
28+
println(Await.result(futureResponse, 5000 millis).asString.split('\n').head)
29+
}
30+
}

examples/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
SafeURL examples
2+
=================
3+
4+
Do the following to run these:
5+
scalac -sourcepath ../src/ BasicExample.scala
6+
scala BasicExample

examples/ResponseExample.scala

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import scala.concurrent.ExecutionContext.Implicits.global
2+
import scala.concurrent.Await
3+
import scala.concurrent.duration._
4+
5+
import com.includesecurity.safeurl.{SafeURL, Response}
6+
7+
/** Example code to demonstrate the usage of [[safeurl.Response]] instances. */
8+
object ResponseExample {
9+
def main(Args: Array[String]) {
10+
val futureResponse = SafeURL.fetch("http://icanhazip.com")
11+
val response = Await.result(futureResponse, 5000 millis)
12+
13+
val header = response.getHeader
14+
println("Header: ")
15+
header.foreach { case (key, list) => println(key + ": " + list.mkString(", ")) }
16+
17+
println()
18+
println("Body: ")
19+
20+
println("As string: " + response.asString.trim)
21+
println("As base64: " + response.asBase64)
22+
println("As bytes: " + response.asBytes.map("%02x " format _).mkString)
23+
24+
println()
25+
26+
println("Writing response to file /tmp/response")
27+
response saveToFile "/tmp/response"
28+
println("Done")
29+
}
30+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/* Configuration.scala - Library to protect agains SSRF.
2+
* Pull requests are welcome, please find this tool hosted on http://github.com/IncludeSecurity
3+
*
4+
* The MIT License (MIT)
5+
*
6+
* Copyright (c) 2014 Samuel Groß
7+
* Copyright (c) 2014 Include Security <info [at sign] includesecurity.com>
8+
*
9+
* Permission is hereby granted, free of charge, to any person obtaining a copy
10+
* of this software and associated documentation files (the "Software"), to deal
11+
* in the Software without restriction, including without limitation the rights
12+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13+
* copies of the Software, and to permit persons to whom the Software is
14+
* furnished to do so, subject to the following conditions:
15+
*
16+
* The above copyright notice and this permission notice shall be included in
17+
* all copies or substantial portions of the Software.
18+
*
19+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25+
* THE SOFTWARE.
26+
*/
27+
28+
package com.includesecurity.safeurl
29+
30+
31+
/** Access lists for the various parts of a URL. */
32+
class AccessList {
33+
var whitelist: List[String] = Nil
34+
var blacklist: List[String] = Nil
35+
}
36+
37+
/** Contains an AccessList for each part of the URL that is validated. */
38+
class ListContainer {
39+
var ip: AccessList = new AccessList
40+
var port: AccessList = new AccessList
41+
var domain: AccessList = new AccessList
42+
var protocol: AccessList = new AccessList
43+
}
44+
45+
/** SafeURL configuration.
46+
*
47+
* Stores the black- and whitelists used by SafeURL as well
48+
* as some other configuration properties.
49+
*
50+
* Has secure defaults.
51+
*/
52+
class Configuration {
53+
/** Do secure redirects, revalidate each redirect location first. */
54+
var secureRedirects: Boolean = true
55+
56+
/** The maximum number of redirects SaveCurl will follow. */
57+
var maxRedirects: Int = 20
58+
59+
/** Determines whether SafeURL will pin DNS entries, preventing DNS rebinding attacks. */
60+
var pinDNS: Boolean = true
61+
62+
/** When a protocol is allowed also allow its default port. */
63+
var allowDefaultPort: Boolean = true
64+
65+
/** Access lists for the various parts of a URL. */
66+
var lists: ListContainer = Configuration.defaultAccessLists
67+
}
68+
69+
object Configuration {
70+
def defaultAccessLists: ListContainer = {
71+
val lists = new ListContainer
72+
lists.ip.blacklist = "0.0.0.0/8" ::
73+
"10.0.0.0/8" ::
74+
"100.64.0.0/10" ::
75+
"127.0.0.0/8" ::
76+
"169.254.0.0/16" ::
77+
"172.16.0.0/12" ::
78+
"192.0.0.0/29" ::
79+
"192.0.2.0/24" ::
80+
"192.88.99.0/24" ::
81+
"192.168.0.0/16" ::
82+
"198.18.0.0/15" ::
83+
"198.51.100.0/24" ::
84+
"203.0.113.0/24" ::
85+
"224.0.0.0/4" ::
86+
"240.0.0.0/4" ::
87+
Nil
88+
89+
lists.port.whitelist = "80" :: "8080" :: "443" :: Nil
90+
lists.protocol.whitelist = "http" :: "https" :: Nil
91+
92+
lists
93+
}
94+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/* Response.scala - Library to protect agains SSRF.
2+
* Pull requests are welcome, please find this tool hosted on http://github.com/IncludeSecurity
3+
*
4+
* The MIT License (MIT)
5+
*
6+
* Copyright (c) 2014 Samuel Groß
7+
* Copyright (c) 2014 Include Security <info [at sign] includesecurity.com>
8+
*
9+
* Permission is hereby granted, free of charge, to any person obtaining a copy
10+
* of this software and associated documentation files (the "Software"), to deal
11+
* in the Software without restriction, including without limitation the rights
12+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13+
* copies of the Software, and to permit persons to whom the Software is
14+
* furnished to do so, subject to the following conditions:
15+
*
16+
* The above copyright notice and this permission notice shall be included in
17+
* all copies or substantial portions of the Software.
18+
*
19+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25+
* THE SOFTWARE.
26+
*/
27+
28+
package com.includesecurity.safeurl
29+
30+
import java.net.URLConnection
31+
import java.nio.file.{Paths, Files}
32+
import java.nio.charset.Charset
33+
import sun.misc.BASE64Encoder
34+
import java.io.{Reader, InputStreamReader}
35+
36+
37+
/** Stores the data returned by the remote server. */
38+
class Response(private val buf: Array[Byte], private val header: Map[String,List[String]]) {
39+
/** Return the header of the response. */
40+
def getHeader: Map[String,List[String]] = header
41+
42+
/** Return the response as a string. */
43+
def asString(charset: Charset = Charset.defaultCharset): String = new String(buf, charset)
44+
def asString: String = asString() // TODO this looks weird, seems to be needed to support response.asString without ()
45+
46+
/** Return the response encoded using base64. */
47+
def asBase64: String = new sun.misc.BASE64Encoder().encode(buf)
48+
49+
/** Return the response as raw byte array. */
50+
def asBytes: Array[Byte] = buf.clone
51+
52+
/** Save the response data to a file.
53+
*
54+
* The file Will be created if it does not already exists. Existing data will be overwritten.
55+
*
56+
* @param path the path of the file to store the data in
57+
* @throws IOException if an I/O error occured while writing to the file
58+
* @throws InvalidPathException if the path string cannot be converted to a Path
59+
*/
60+
def saveToFile(path: String): Unit = Files.write(Paths.get(path), buf)
61+
}

0 commit comments

Comments
 (0)