Skip to content

Commit c2c3600

Browse files
committed
feat: sync selected upstream MCP improvements
1 parent c36307a commit c2c3600

10 files changed

Lines changed: 718 additions & 60 deletions

File tree

src/main/kotlin/net/portswigger/mcp/config/McpConfig.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class McpConfig(storage: PersistedObject, private val logging: Logging) {
3737
var keepaliveIntervalSec by storage.int(30)
3838
var maxResponseSizeKb by storage.int(100)
3939
var strictLocalhostMode by storage.boolean(true)
40+
var filterConfigCredentials by storage.boolean(true)
4041
var advancedMcpTools by storage.boolean(true)
4142
var graphqlMcpTools by storage.boolean(true)
4243
var exportInScopeOnly by storage.boolean(false)
@@ -79,6 +80,16 @@ class McpConfig(storage: PersistedObject, private val logging: Logging) {
7980
}
8081
}
8182

83+
private var _alwaysAllowOrganizer by storage.boolean(false)
84+
var alwaysAllowOrganizer: Boolean
85+
get() = _alwaysAllowOrganizer
86+
set(value) {
87+
if (_alwaysAllowOrganizer != value) {
88+
_alwaysAllowOrganizer = value
89+
notifyHistoryAccessChanged()
90+
}
91+
}
92+
8293
private var _autoApproveTargets by storage.stringList("")
8394
private val targetsChangeListeners = CopyOnWriteArrayList<ListenerRegistration>()
8495
private val historyAccessChangeListeners = CopyOnWriteArrayList<ListenerRegistration>()

src/main/kotlin/net/portswigger/mcp/config/components/ServerConfigurationPanel.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class ServerConfigurationPanel(
2121

2222
private lateinit var alwaysAllowHttpHistoryCheckBox: JCheckBox
2323
private lateinit var alwaysAllowWebSocketHistoryCheckBox: JCheckBox
24+
private lateinit var alwaysAllowOrganizerCheckBox: JCheckBox
2425
private lateinit var exportInScopeOnlyCheckBox: JCheckBox
2526
private lateinit var exportNoiseModeComboBox: JComboBox<ExportNoiseModeOption>
2627
private lateinit var saveRawDuplicatesCheckBox: JCheckBox
@@ -49,6 +50,14 @@ class ServerConfigurationPanel(
4950
add(configEditingToolingCheckBox)
5051
add(createVerticalStrut(Design.Spacing.MD))
5152

53+
val filterConfigCredentialsCheckBox = createCheckBoxWithSubtitle(
54+
"过滤配置输出中的凭据",
55+
"默认隐藏密码、证书密码和 REST API 密钥哈希",
56+
config.filterConfigCredentials
57+
) { config.filterConfigCredentials = it }
58+
add(filterConfigCredentialsCheckBox)
59+
add(createVerticalStrut(Design.Spacing.MD))
60+
5261
val httpRequestApprovalCheckBox = createStandardCheckBox(
5362
"HTTP 请求需要审批", config.requireHttpRequestApproval
5463
) { enabled ->
@@ -74,6 +83,14 @@ class ServerConfigurationPanel(
7483
config.requireHistoryAccessApproval
7584
) { config.alwaysAllowWebSocketHistory = it }
7685
add(alwaysAllowWebSocketHistoryCheckBox)
86+
add(createVerticalStrut(Design.Spacing.SM))
87+
88+
alwaysAllowOrganizerCheckBox = createIndentedCheckBox(
89+
"始终允许 Organizer 访问",
90+
config.alwaysAllowOrganizer,
91+
config.requireHistoryAccessApproval
92+
) { config.alwaysAllowOrganizer = it }
93+
add(alwaysAllowOrganizerCheckBox)
7794
add(createVerticalStrut(Design.Spacing.MD))
7895

7996
exportInScopeOnlyCheckBox = createStandardCheckBox(
@@ -112,18 +129,22 @@ class ServerConfigurationPanel(
112129
if (!enabled) {
113130
config.alwaysAllowHttpHistory = false
114131
config.alwaysAllowWebSocketHistory = false
132+
config.alwaysAllowOrganizer = false
115133
alwaysAllowHttpHistoryCheckBox.isSelected = false
116134
alwaysAllowWebSocketHistoryCheckBox.isSelected = false
135+
alwaysAllowOrganizerCheckBox.isSelected = false
117136
}
118137
alwaysAllowHttpHistoryCheckBox.isEnabled = enabled
119138
alwaysAllowWebSocketHistoryCheckBox.isEnabled = enabled
139+
alwaysAllowOrganizerCheckBox.isEnabled = enabled
120140
}
121141
}
122142

123143
fun updateHistoryAccessCheckboxes() {
124144
SwingUtilities.invokeLater {
125145
alwaysAllowHttpHistoryCheckBox.isSelected = config.alwaysAllowHttpHistory
126146
alwaysAllowWebSocketHistoryCheckBox.isSelected = config.alwaysAllowWebSocketHistory
147+
alwaysAllowOrganizerCheckBox.isSelected = config.alwaysAllowOrganizer
127148
}
128149
}
129150

src/main/kotlin/net/portswigger/mcp/exporter/Exporter.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ private val LOW_VALUE_BROWSER_PATH_FRAGMENTS = setOf(
3636
)
3737

3838
private val NOISE_QUERY_KEYS = setOf(
39-
"_", "t", "ts", "timestamp", "cb", "cacheBust", "cache_bust", "nonce", "rnd"
39+
"_", "t", "ts", "_ts", "timestamp", "cb", "cacheBust", "cache_bust", "nonce", "rnd"
4040
)
4141

4242
private val SENSITIVE_RESPONSE_MARKERS = listOf(

src/main/kotlin/net/portswigger/mcp/providers/Provider.kt

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import java.nio.file.Path
99
import java.nio.file.StandardCopyOption
1010
import javax.swing.JFileChooser
1111
import kotlin.io.path.exists
12+
import kotlin.io.path.isDirectory
13+
import kotlin.io.path.listDirectoryEntries
14+
import kotlin.io.path.name
1215
import kotlin.io.path.readText
1316
import kotlin.io.path.writeText
1417

@@ -68,14 +71,24 @@ class ClaudeDesktopProvider(private val logging: Logging, private val proxyJarMa
6871
val os = System.getProperty("os.name").lowercase()
6972
val home = System.getProperty("user.home")
7073

71-
val basePath = when {
72-
os.contains("win") -> Path.of(home, "AppData", "Roaming", "Claude")
73-
os.contains("mac") || os.contains("darwin") -> Path.of(home, "Library", "Application Support", "Claude")
74-
os.contains("linux") -> Path.of(home, ".config", "Claude")
74+
val candidatePaths = when {
75+
os.contains("win") -> windowsCandidatePaths(home)
76+
os.contains("mac") || os.contains("darwin") -> listOf(
77+
Path.of(home, "Library", "Application Support", "Claude")
78+
)
79+
os.contains("linux") -> listOf(Path.of(home, ".config", "Claude"))
7580
else -> return null
7681
}
7782

78-
if (!basePath.exists()) return null
83+
val existingPaths = candidatePaths.filter { it.exists() && it.isDirectory() }
84+
val pathsWithConfig = existingPaths.filter { it.resolve(claudeConfigFileName).exists() }
85+
val preferredPaths = pathsWithConfig.ifEmpty { existingPaths }
86+
if (preferredPaths.size > 1) {
87+
logging.logToOutput(
88+
"Warning: multiple Claude Desktop config directories found; using ${preferredPaths.first()}: $preferredPaths"
89+
)
90+
}
91+
val basePath = preferredPaths.firstOrNull() ?: return null
7992

8093
val configFile = basePath.resolve(claudeConfigFileName)
8194
if (!configFile.exists()) {
@@ -85,6 +98,24 @@ class ClaudeDesktopProvider(private val logging: Logging, private val proxyJarMa
8598
return configFile
8699
}
87100

101+
internal fun windowsCandidatePaths(home: String): List<Path> {
102+
val traditionalPath = Path.of(home, "AppData", "Roaming", "Claude")
103+
val packagesDirectory = Path.of(home, "AppData", "Local", "Packages")
104+
val storePaths = if (packagesDirectory.exists() && packagesDirectory.isDirectory()) {
105+
runCatching {
106+
packagesDirectory.listDirectoryEntries()
107+
.filter { it.isDirectory() && it.name.startsWith("Claude_") }
108+
.map { it.resolve("LocalCache").resolve("Roaming").resolve("Claude") }
109+
}.getOrElse { error ->
110+
logging.logToError("Failed to inspect Windows Store Claude packages: ${error.message}")
111+
emptyList()
112+
}
113+
} else {
114+
emptyList()
115+
}
116+
return listOf(traditionalPath) + storePaths
117+
}
118+
88119
private fun createDefaultConfig(path: Path): Boolean {
89120
try {
90121
val defaultConfig = buildJsonObject {
@@ -146,4 +177,4 @@ class ManualProxyInstallerProvider(private val logging: Logging, private val pro
146177

147178
return "代理 jar 已提取到 $destinationFile"
148179
}
149-
}
180+
}

src/main/kotlin/net/portswigger/mcp/security/HistoryAccessSecurity.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import kotlin.coroutines.resume
77
import kotlin.coroutines.suspendCoroutine
88

99
enum class HistoryAccessType() {
10-
HTTP_HISTORY(), WEBSOCKET_HISTORY();
10+
HTTP_HISTORY(), WEBSOCKET_HISTORY(), ORGANIZER();
1111
}
1212

1313
interface HistoryAccessApprovalHandler {
@@ -23,6 +23,7 @@ class SwingHistoryAccessApprovalHandler : HistoryAccessApprovalHandler {
2323
val historyTypeName = when (accessType) {
2424
HistoryAccessType.HTTP_HISTORY -> "HTTP history"
2525
HistoryAccessType.WEBSOCKET_HISTORY -> "WebSocket history"
26+
HistoryAccessType.ORGANIZER -> "Organizer items"
2627
}
2728

2829
val message = buildString {
@@ -51,6 +52,7 @@ class SwingHistoryAccessApprovalHandler : HistoryAccessApprovalHandler {
5152
when (accessType) {
5253
HistoryAccessType.HTTP_HISTORY -> config.alwaysAllowHttpHistory = true
5354
HistoryAccessType.WEBSOCKET_HISTORY -> config.alwaysAllowWebSocketHistory = true
55+
HistoryAccessType.ORGANIZER -> config.alwaysAllowOrganizer = true
5456
}
5557
continuation.resume(true)
5658
}
@@ -78,6 +80,7 @@ object HistoryAccessSecurity {
7880
val isAlwaysAllowed = when (accessType) {
7981
HistoryAccessType.HTTP_HISTORY -> config.alwaysAllowHttpHistory
8082
HistoryAccessType.WEBSOCKET_HISTORY -> config.alwaysAllowWebSocketHistory
83+
HistoryAccessType.ORGANIZER -> config.alwaysAllowOrganizer
8184
}
8285

8386
if (isAlwaysAllowed) {
@@ -86,4 +89,4 @@ object HistoryAccessSecurity {
8689

8790
return approvalHandler.requestHistoryAccess(accessType, config)
8891
}
89-
}
92+
}

src/main/kotlin/net/portswigger/mcp/security/SecurityUtils.kt

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
package net.portswigger.mcp.security
22

3+
import kotlinx.serialization.SerializationException
4+
import kotlinx.serialization.json.Json
5+
import kotlinx.serialization.json.JsonArray
6+
import kotlinx.serialization.json.JsonElement
7+
import kotlinx.serialization.json.JsonObject
8+
import kotlinx.serialization.json.JsonPrimitive
39
import java.awt.Frame
410

511
/**
@@ -17,4 +23,33 @@ fun findBurpFrame(): Frame? {
1723
} ?: Frame.getFrames()
1824
.filter { it.isVisible && it.isDisplayable }
1925
.maxByOrNull { it.width * it.height }
20-
}
26+
}
27+
28+
private val SENSITIVE_CONFIG_KEYS = setOf(
29+
"password",
30+
"certificate_password",
31+
"hashed_key"
32+
)
33+
34+
private const val REDACTED_CONFIG_VALUE = "*****"
35+
36+
fun filterConfigCredentials(json: String): String {
37+
return try {
38+
Json.encodeToString(filterConfigElement(Json.parseToJsonElement(json)))
39+
} catch (_: SerializationException) {
40+
"""{"error":"failed to parse config json"}"""
41+
}
42+
}
43+
44+
private fun filterConfigElement(element: JsonElement): JsonElement = when (element) {
45+
is JsonObject -> JsonObject(element.mapValues { (key, value) -> filterConfigValue(key, value) })
46+
is JsonArray -> JsonArray(element.map(::filterConfigElement))
47+
else -> element
48+
}
49+
50+
private fun filterConfigValue(key: String, value: JsonElement): JsonElement =
51+
if (key.lowercase() in SENSITIVE_CONFIG_KEYS && value is JsonPrimitive && value.isString) {
52+
JsonPrimitive(REDACTED_CONFIG_VALUE)
53+
} else {
54+
filterConfigElement(value)
55+
}

0 commit comments

Comments
 (0)