at://nekomimi.pet/sh.tangled.repo.pull/3magz3ojgec22
Back to Collection
Record JSON
{
"$type": "sh.tangled.repo.pull",
"createdAt": "2025-12-20T20:08:52Z",
"patch": "From 74bf70939b59bd6e0bbf678217b2eff78636023d Mon Sep 17 00:00:00 2001\nFrom: \"@nekomimi.pet\" \u003cmeowskulls@nekomimi.pet\u003e\nDate: Sat, 20 Dec 2025 15:00:55 -0500\nSubject: [PATCH] ssrf\n\n---\n .../jollywhoppers/atproto/AtProtoClient.kt | 69 ++++++++++++++++++-\n 1 file changed, 67 insertions(+), 2 deletions(-)\n\ndiff --git a/src/main/kotlin/com/jollywhoppers/atproto/AtProtoClient.kt b/src/main/kotlin/com/jollywhoppers/atproto/AtProtoClient.kt\nindex 7854db7..a9017ba 100644\n--- a/src/main/kotlin/com/jollywhoppers/atproto/AtProtoClient.kt\n+++ b/src/main/kotlin/com/jollywhoppers/atproto/AtProtoClient.kt\n@@ -159,7 +159,7 @@ class AtProtoClient(\n */\n suspend fun resolveDid(did: String): Result\u003cDidDocument\u003e = runCatching {\n logger.info(\"Resolving DID: $did\")\n- \n+\n val url = when {\n did.startsWith(\"did:plc:\") -\u003e {\n val identifier = did.removePrefix(\"did:plc:\")\n@@ -167,6 +167,15 @@ class AtProtoClient(\n }\n did.startsWith(\"did:web:\") -\u003e {\n val domain = did.removePrefix(\"did:web:\")\n+\n+ // Validate domain format (no IPs, only valid hostnames)\n+ if (!domain.matches(Regex(\"^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\\\\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$\"))) {\n+ throw IllegalArgumentException(\"Invalid did:web domain format: must be a valid hostname\")\n+ }\n+\n+ // Block private IP ranges and localhost\n+ validateNotPrivateNetwork(domain)\n+\n \"https://$domain/.well-known/did.json\"\n }\n else -\u003e throw IllegalArgumentException(\"Unsupported DID method: $did\")\n@@ -363,7 +372,41 @@ class AtProtoClient(\n ?: throw Exception(\"No handle found in DID document\")\n val pdsService = didDoc.service.firstOrNull { it.type == \"AtprotoPersonalDataServer\" }\n ?: throw Exception(\"No PDS service found in DID document\")\n- Triple(identifier, handle, pdsService.serviceEndpoint)\n+\n+ // Validate serviceEndpoint per AT Protocol spec\n+ val serviceEndpoint = pdsService.serviceEndpoint\n+ val uri = try {\n+ URI.create(serviceEndpoint)\n+ } catch (e: Exception) {\n+ throw Exception(\"Invalid serviceEndpoint URI: ${e.message}\")\n+ }\n+\n+ // Validate per AT Protocol spec\n+ require(uri.scheme in listOf(\"http\", \"https\")) {\n+ \"serviceEndpoint must use HTTP or HTTPS scheme, got: ${uri.scheme}\"\n+ }\n+ require(uri.host != null) {\n+ \"serviceEndpoint must have a valid host\"\n+ }\n+ require(uri.path.isNullOrEmpty() || uri.path == \"/\") {\n+ \"serviceEndpoint must not contain path, got: ${uri.path}\"\n+ }\n+ require(uri.query == null) {\n+ \"serviceEndpoint must not contain query parameters\"\n+ }\n+ require(uri.fragment == null) {\n+ \"serviceEndpoint must not contain fragment\"\n+ }\n+ require(uri.userInfo == null) {\n+ \"serviceEndpoint must not contain userinfo\"\n+ }\n+\n+ // Block private IP ranges\n+ validateNotPrivateNetwork(uri.host)\n+\n+ // Reconstruct clean URL\n+ val cleanPdsUrl = \"${uri.scheme}://${uri.host}${uri.port.takeIf { it != -1 }?.let { \":$it\" } ?: \"\"}\"\n+ Triple(identifier, handle, cleanPdsUrl)\n }\n }\n else -\u003e {\n@@ -382,4 +425,26 @@ class AtProtoClient(\n .rawSchemeSpecificPart\n .replace(\"+\", \"%20\")\n }\n+\n+ /**\n+ * Validates that a hostname or domain is not a private network address.\n+ * Throws IllegalArgumentException if the address is localhost or a private IP range.\n+ */\n+ private fun validateNotPrivateNetwork(host: String) {\n+ val blockedPatterns = listOf(\n+ Regex(\"^localhost$\", RegexOption.IGNORE_CASE),\n+ Regex(\"^127\\\\.\"),\n+ Regex(\"^10\\\\.\"),\n+ Regex(\"^172\\\\.(1[6-9]|2[0-9]|3[01])\\\\.\"),\n+ Regex(\"^192\\\\.168\\\\.\"),\n+ Regex(\"^169\\\\.254\\\\.\"),\n+ Regex(\"^::1$\"),\n+ Regex(\"^fc00:\"),\n+ Regex(\"^fe80:\")\n+ )\n+\n+ if (blockedPatterns.any { it.containsMatchIn(host) }) {\n+ throw IllegalArgumentException(\"Access to private networks is not allowed: $host\")\n+ }\n+ }\n }\n-- \n2.51.2\n\n\n",
"source": {
"branch": "main",
"repo": "at://did:plc:ttdrpj45ibqunmfhdsb4zdwq/sh.tangled.repo/3magyppadc322",
"sha": "74bf70939b59bd6e0bbf678217b2eff78636023d"
},
"target": {
"branch": "main",
"repo": "at://did:plc:ofrbh253gwicbkc5nktqepol/sh.tangled.repo/3mag5vca2pd22"
},
"title": "ssrf validation"
}