tangled
alpha
login
or
join now
jollywhoppers.com
/
socialsync
6
fork
atom
A Minecraft Fabric mod that connects the game with ATProtocol ⛏️
6
fork
atom
overview
issues
pulls
pipelines
ssrf
authored by
@nekomimi.pet
and committed by
tangled.org
2 months ago
2d1b3844
c0873456
+67
-2
1 changed file
expand all
collapse all
unified
split
src
main
kotlin
com
jollywhoppers
atproto
AtProtoClient.kt
+67
-2
src/main/kotlin/com/jollywhoppers/atproto/AtProtoClient.kt
···
159
159
*/
160
160
suspend fun resolveDid(did: String): Result<DidDocument> = runCatching {
161
161
logger.info("Resolving DID: $did")
162
162
-
162
162
+
163
163
val url = when {
164
164
did.startsWith("did:plc:") -> {
165
165
val identifier = did.removePrefix("did:plc:")
···
167
167
}
168
168
did.startsWith("did:web:") -> {
169
169
val domain = did.removePrefix("did:web:")
170
170
+
171
171
+
// Validate domain format (no IPs, only valid hostnames)
172
172
+
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])?)*$"))) {
173
173
+
throw IllegalArgumentException("Invalid did:web domain format: must be a valid hostname")
174
174
+
}
175
175
+
176
176
+
// Block private IP ranges and localhost
177
177
+
validateNotPrivateNetwork(domain)
178
178
+
170
179
"https://$domain/.well-known/did.json"
171
180
}
172
181
else -> throw IllegalArgumentException("Unsupported DID method: $did")
···
363
372
?: throw Exception("No handle found in DID document")
364
373
val pdsService = didDoc.service.firstOrNull { it.type == "AtprotoPersonalDataServer" }
365
374
?: throw Exception("No PDS service found in DID document")
366
366
-
Triple(identifier, handle, pdsService.serviceEndpoint)
375
375
+
376
376
+
// Validate serviceEndpoint per AT Protocol spec
377
377
+
val serviceEndpoint = pdsService.serviceEndpoint
378
378
+
val uri = try {
379
379
+
URI.create(serviceEndpoint)
380
380
+
} catch (e: Exception) {
381
381
+
throw Exception("Invalid serviceEndpoint URI: ${e.message}")
382
382
+
}
383
383
+
384
384
+
// Validate per AT Protocol spec
385
385
+
require(uri.scheme in listOf("http", "https")) {
386
386
+
"serviceEndpoint must use HTTP or HTTPS scheme, got: ${uri.scheme}"
387
387
+
}
388
388
+
require(uri.host != null) {
389
389
+
"serviceEndpoint must have a valid host"
390
390
+
}
391
391
+
require(uri.path.isNullOrEmpty() || uri.path == "/") {
392
392
+
"serviceEndpoint must not contain path, got: ${uri.path}"
393
393
+
}
394
394
+
require(uri.query == null) {
395
395
+
"serviceEndpoint must not contain query parameters"
396
396
+
}
397
397
+
require(uri.fragment == null) {
398
398
+
"serviceEndpoint must not contain fragment"
399
399
+
}
400
400
+
require(uri.userInfo == null) {
401
401
+
"serviceEndpoint must not contain userinfo"
402
402
+
}
403
403
+
404
404
+
// Block private IP ranges
405
405
+
validateNotPrivateNetwork(uri.host)
406
406
+
407
407
+
// Reconstruct clean URL
408
408
+
val cleanPdsUrl = "${uri.scheme}://${uri.host}${uri.port.takeIf { it != -1 }?.let { ":$it" } ?: ""}"
409
409
+
Triple(identifier, handle, cleanPdsUrl)
367
410
}
368
411
}
369
412
else -> {
···
381
424
.resolve(value)
382
425
.rawSchemeSpecificPart
383
426
.replace("+", "%20")
427
427
+
}
428
428
+
429
429
+
/**
430
430
+
* Validates that a hostname or domain is not a private network address.
431
431
+
* Throws IllegalArgumentException if the address is localhost or a private IP range.
432
432
+
*/
433
433
+
private fun validateNotPrivateNetwork(host: String) {
434
434
+
val blockedPatterns = listOf(
435
435
+
Regex("^localhost$", RegexOption.IGNORE_CASE),
436
436
+
Regex("^127\\."),
437
437
+
Regex("^10\\."),
438
438
+
Regex("^172\\.(1[6-9]|2[0-9]|3[01])\\."),
439
439
+
Regex("^192\\.168\\."),
440
440
+
Regex("^169\\.254\\."),
441
441
+
Regex("^::1$"),
442
442
+
Regex("^fc00:"),
443
443
+
Regex("^fe80:")
444
444
+
)
445
445
+
446
446
+
if (blockedPatterns.any { it.containsMatchIn(host) }) {
447
447
+
throw IllegalArgumentException("Access to private networks is not allowed: $host")
448
448
+
}
384
449
}
385
450
}