mirror of
https://github.com/PortSwigger/mcp-server
synced 2026-06-21 13:45:21 +00:00
Broaden credential filter and fail closed on malformed JSON
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
package net.portswigger.mcp.security
|
package net.portswigger.mcp.security
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerializationException
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonArray
|
import kotlinx.serialization.json.JsonArray
|
||||||
import kotlinx.serialization.json.JsonElement
|
import kotlinx.serialization.json.JsonElement
|
||||||
@@ -24,36 +25,36 @@ fun findBurpFrame(): Frame? {
|
|||||||
.maxByOrNull { it.width * it.height }
|
.maxByOrNull { it.width * it.height }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keys that hold credential-bearing values in Burp's exported user/project options.
|
||||||
|
// Cross-referenced against SuiteConfigurationFragmentFields in the Burp desktop codebase.
|
||||||
|
private val SENSITIVE_KEYS = setOf(
|
||||||
|
"password", // socks proxy, platform auth, upstream proxy, client certs, app login
|
||||||
|
"certificate_password", // proxy request listener PKCS12 password
|
||||||
|
"hashed_key", // Burp REST API key (SHA-256 of the actual key)
|
||||||
|
)
|
||||||
|
|
||||||
|
private const val REDACTED = "*****"
|
||||||
|
|
||||||
fun filterConfigCredentials(json: String): String {
|
fun filterConfigCredentials(json: String): String {
|
||||||
return try {
|
return try {
|
||||||
val jsonElement = Json.parseToJsonElement(json)
|
Json.encodeToString(filterJsonElement(Json.parseToJsonElement(json)))
|
||||||
val filteredElement = filterJsonElement(jsonElement)
|
} catch (_: SerializationException) {
|
||||||
Json.encodeToString(filteredElement)
|
// Burp's export is expected to always be valid JSON. If it isn't, fail closed:
|
||||||
} catch (e: Exception) {
|
// do not echo the original or the parser's message (it quotes surrounding input,
|
||||||
throw RuntimeException("Failed to filter credentials", e)
|
// which can include credential values).
|
||||||
|
"""{"error":"failed to parse config json"}"""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun filterJsonElement(element: JsonElement): JsonElement {
|
private fun filterJsonElement(element: JsonElement): JsonElement = when (element) {
|
||||||
return when (element) {
|
is JsonObject -> JsonObject(element.mapValues { (key, value) -> filterValue(key, value) })
|
||||||
is JsonObject -> filterJsonObject(element)
|
is JsonArray -> JsonArray(element.map(::filterJsonElement))
|
||||||
is JsonArray -> filterJsonArray(element)
|
else -> element
|
||||||
else -> element
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun filterJsonObject(obj: JsonObject): JsonObject {
|
private fun filterValue(key: String, value: JsonElement): JsonElement =
|
||||||
val filteredMap = obj.mapValues { (key, value) ->
|
if (key.lowercase() in SENSITIVE_KEYS && value is JsonPrimitive && value.isString) {
|
||||||
when {
|
JsonPrimitive(REDACTED)
|
||||||
value is JsonPrimitive && value.isString && key == "password" ->
|
} else {
|
||||||
JsonPrimitive("*****")
|
filterJsonElement(value)
|
||||||
else -> filterJsonElement(value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return JsonObject(filteredMap)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun filterJsonArray(array: JsonArray): JsonArray {
|
|
||||||
val filteredList = array.map { element -> filterJsonElement(element) }
|
|
||||||
return JsonArray(filteredList)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -251,61 +251,58 @@ class CredentialFilterTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test security filter with malformed Json on user_options`() {
|
fun `test security filter with malformed Json on user_options`() {
|
||||||
config.filterConfigCredentials = true
|
|
||||||
val malformedJson = """
|
val malformedJson = """
|
||||||
{
|
{
|
||||||
"user_options": {
|
"user_options": {
|
||||||
"bchecks": {},
|
|
||||||
"connections": {
|
"connections": {
|
||||||
"platform_authentication": {
|
"socks_proxy": { "password": "leakme"
|
||||||
"credentials": []
|
}
|
||||||
},
|
|
||||||
"socks_proxy": { "password": ""
|
|
||||||
},
|
|
||||||
"display": {},
|
|
||||||
"extender": {},
|
|
||||||
"intruder": {},
|
|
||||||
"misc": {},
|
|
||||||
"proxy": {},
|
|
||||||
"repeater": {},
|
|
||||||
"ssl": {}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
val exception = Assertions.assertThrows(RuntimeException::class.java) {
|
val result = filterConfigCredentials(malformedJson)
|
||||||
filterConfigCredentials(malformedJson)
|
Assertions.assertFalse(result.contains("leakme"), "Original input must not be echoed on parse failure")
|
||||||
}
|
val parsed = Json.parseToJsonElement(result).jsonObject
|
||||||
Assertions.assertEquals("Failed to filter credentials", exception.message)
|
Assertions.assertNotNull(parsed["error"])
|
||||||
Assertions.assertNotNull(exception.cause)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test security filter with malformed Json on project_options`() {
|
fun `test security filter with malformed Json on project_options`() {
|
||||||
config.filterConfigCredentials = true
|
|
||||||
val malformedJson = """
|
val malformedJson = """
|
||||||
{
|
{
|
||||||
"bambda": {},
|
|
||||||
"logger": {},
|
|
||||||
"organiser": {},
|
|
||||||
"project_options": {
|
"project_options": {
|
||||||
"connections": {
|
"connections": {
|
||||||
"platform_authentication": {
|
"socks_proxy": { "password": "leakme" }
|
||||||
"credentials": []
|
|
||||||
},
|
|
||||||
"socks_proxy": { "password": "" }
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"proxy": {},
|
|
||||||
"repeater": {},
|
|
||||||
"sequencer": {},
|
|
||||||
"target": {}
|
|
||||||
|
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
val exception = Assertions.assertThrows(RuntimeException::class.java) {
|
val result = filterConfigCredentials(malformedJson)
|
||||||
filterConfigCredentials(malformedJson)
|
Assertions.assertFalse(result.contains("leakme"), "Original input must not be echoed on parse failure")
|
||||||
|
val parsed = Json.parseToJsonElement(result).jsonObject
|
||||||
|
Assertions.assertNotNull(parsed["error"])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `proxy listener certificate password and REST API hashed key are redacted`() {
|
||||||
|
val input = """
|
||||||
|
{
|
||||||
|
"proxy": { "request_listeners": [ { "certificate_password": "p12pass" } ] },
|
||||||
|
"misc": { "api": { "keys": [ { "name": "k1", "hashed_key": "deadbeef" } ] } }
|
||||||
}
|
}
|
||||||
Assertions.assertEquals("Failed to filter credentials", exception.message)
|
""".trimIndent()
|
||||||
Assertions.assertNotNull(exception.cause)
|
val parsed = Json.parseToJsonElement(filterConfigCredentials(input)).jsonObject
|
||||||
|
val listener = parsed["proxy"]!!.jsonObject["request_listeners"]!!.jsonArray[0].jsonObject
|
||||||
|
Assertions.assertEquals("*****", listener["certificate_password"]?.jsonPrimitive?.content)
|
||||||
|
val apiKey = parsed["misc"]!!.jsonObject["api"]!!.jsonObject["keys"]!!.jsonArray[0].jsonObject
|
||||||
|
Assertions.assertEquals("*****", apiKey["hashed_key"]?.jsonPrimitive?.content)
|
||||||
|
Assertions.assertEquals("k1", apiKey["name"]?.jsonPrimitive?.content)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `sensitive key matching is case insensitive`() {
|
||||||
|
val input = """{"Password":"a","Certificate_Password":"b","HASHED_KEY":"c"}"""
|
||||||
|
val parsed = Json.parseToJsonElement(filterConfigCredentials(input)).jsonObject
|
||||||
|
Assertions.assertEquals("*****", parsed["Password"]?.jsonPrimitive?.content)
|
||||||
|
Assertions.assertEquals("*****", parsed["Certificate_Password"]?.jsonPrimitive?.content)
|
||||||
|
Assertions.assertEquals("*****", parsed["HASHED_KEY"]?.jsonPrimitive?.content)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user