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"
}