Skip to content

Commit fe5f087

Browse files
authored
security: require https for WebDAV endpoints (#5)
* security: require https for WebDAV endpoints A WebDAV backup contains every saved profile (all proxy secrets) and credentials are sent via HTTP Basic auth. Plain http:// transmitted both in cleartext. - Add WebDAVSecurity.requireSecureUrl() rejecting non-https endpoints with a localized, credential-free error. - Apply it to backup, restore, and the connection test. - Remove now-unused URL/HttpUrl imports. Credentials are never written to logs (Authorization header is not logged; only scheme/host URLs and response codes are), so no additional redaction is needed. * security: address review - remove stale URL import, add locale strings - Remove unused java.net.URL import from BackupFragment. - Add webdav_insecure_url translations to all 6 locale files (zh-rCN, zh-rTW, ru, ja, ko, fa) so the security message is not English-only for non-English users.
1 parent 0599062 commit fe5f087

10 files changed

Lines changed: 50 additions & 24 deletions

File tree

app/src/main/java/io/nekohasekai/sagernet/ui/BackupFragment.kt

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ import org.json.JSONArray
3030
import org.json.JSONObject
3131
import java.io.ByteArrayOutputStream
3232
import java.io.File
33-
import java.net.URL
3433
import java.util.*
3534
import okhttp3.Credentials
3635
import okhttp3.OkHttpClient
@@ -47,7 +46,6 @@ import java.util.zip.ZipInputStream
4746
import java.util.concurrent.TimeUnit
4847
import java.util.zip.Deflater
4948
import java.io.BufferedOutputStream
50-
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
5149

5250
class BackupFragment : NamedFragment(R.layout.layout_backup) {
5351

@@ -208,14 +206,9 @@ class BackupFragment : NamedFragment(R.layout.layout_backup) {
208206
val version = BuildConfig.VERSION_NAME
209207
val fileName = "nekobox_backup_${version}_$timestamp.zip"
210208

211-
// 确保 baseUrl 是有效的 URL
212-
if (!baseUrl.startsWith("http://") && !baseUrl.startsWith("https://")) {
213-
throw Exception("Invalid server URL: must start with http:// or https://")
214-
}
215-
216-
// 使用 HttpUrl 构建路径,避免 # 等特殊字符被当作 fragment
217-
val baseHttpUrl = baseUrl.toHttpUrlOrNull()
218-
?: throw Exception("Invalid server URL: $baseUrl")
209+
// 确保 baseUrl 是有效的且使用 TLS 的 URL
210+
// (WebDAV 备份包含所有配置密钥,禁止明文 http://)
211+
val baseHttpUrl = WebDAVSecurity.requireSecureUrl(baseUrl)
219212

220213
val dirUrl = baseHttpUrl.newBuilder().apply {
221214
path.split('/').filter { it.isNotEmpty() }.forEach { segment ->
@@ -339,12 +332,8 @@ class BackupFragment : NamedFragment(R.layout.layout_backup) {
339332
val baseUrl = DataStore.webdavServer!!.trimEnd('/')
340333
val path = DataStore.webdavPath?.trim('/')?.takeIf { it.isNotEmpty() } ?: "Nekobox"
341334

342-
if (!baseUrl.startsWith("http://") && !baseUrl.startsWith("https://")) {
343-
throw Exception("Invalid server URL: must start with http:// or https://")
344-
}
345-
346-
val baseHttpUrl = baseUrl.toHttpUrlOrNull()
347-
?: throw Exception("Invalid server URL: $baseUrl")
335+
// WebDAV 备份包含所有配置密钥,禁止明文 http://
336+
val baseHttpUrl = WebDAVSecurity.requireSecureUrl(baseUrl)
348337

349338
val dirUrl = baseHttpUrl.newBuilder().apply {
350339
path.split('/').filter { it.isNotEmpty() }.forEach { segment ->
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package io.nekohasekai.sagernet.ui
2+
3+
import io.nekohasekai.sagernet.R
4+
import io.nekohasekai.sagernet.SagerNet
5+
import okhttp3.HttpUrl
6+
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
7+
8+
/**
9+
* Security policy for WebDAV endpoints.
10+
*
11+
* A WebDAV backup contains every saved profile, including all proxy secrets, and
12+
* the credentials are sent via HTTP Basic auth. Allowing a plain `http://` endpoint
13+
* would transmit both the credentials and the secret-bearing backup in cleartext,
14+
* so only TLS (`https://`) endpoints are accepted.
15+
*/
16+
object WebDAVSecurity {
17+
18+
/**
19+
* Validates and parses a user-provided WebDAV server URL.
20+
*
21+
* @return the parsed [HttpUrl] when the URL is a well-formed `https://` endpoint.
22+
* @throws Exception with a localized message when the scheme is not `https`
23+
* or the URL is malformed. The message intentionally contains no credentials.
24+
*/
25+
fun requireSecureUrl(rawUrl: String): HttpUrl {
26+
val url = rawUrl.trimEnd('/').toHttpUrlOrNull()
27+
?: throw Exception(SagerNet.application.getString(R.string.webdav_insecure_url))
28+
if (!url.isHttps) {
29+
throw Exception(SagerNet.application.getString(R.string.webdav_insecure_url))
30+
}
31+
return url
32+
}
33+
}

app/src/main/java/io/nekohasekai/sagernet/ui/WebDAVSettingsActivity.kt

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,10 @@ import kotlinx.coroutines.launch
1919
import okhttp3.Credentials
2020
import okhttp3.OkHttpClient
2121
import okhttp3.Request
22-
import java.net.URL
2322
import com.google.android.material.snackbar.Snackbar
2423
import okhttp3.MediaType.Companion.toMediaType
2524
import okhttp3.RequestBody.Companion.toRequestBody
2625
import java.util.concurrent.TimeUnit
27-
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
2826

2927
class WebDAVSettingsActivity : ThemedActivity() {
3028

@@ -127,7 +125,9 @@ class WebDAVSettingsActivity : ThemedActivity() {
127125
throw Exception(getString(R.string.webdav_server_empty))
128126
}
129127

130-
val url = URL(server)
128+
// Reject plain http:// — the test sends Basic-auth credentials.
129+
val secureUrl = WebDAVSecurity.requireSecureUrl(server)
130+
131131
val client = OkHttpClient.Builder()
132132
.connectTimeout(10, TimeUnit.SECONDS)
133133
.readTimeout(10, TimeUnit.SECONDS)
@@ -136,7 +136,7 @@ class WebDAVSettingsActivity : ThemedActivity() {
136136

137137
// 首先测试连接和认证
138138
val authRequest = Request.Builder()
139-
.url(url)
139+
.url(secureUrl)
140140
.method("PROPFIND", null)
141141
.apply {
142142
val credentials = Credentials.basic(
@@ -164,10 +164,7 @@ class WebDAVSettingsActivity : ThemedActivity() {
164164
// 如果认证成功,再测试目录操作
165165
val path = (DataStore.webdavPath ?: "").trim('/')
166166
if (path.isNotBlank()) {
167-
val baseHttpUrl = server.toHttpUrlOrNull()
168-
?: throw Exception(getString(R.string.webdav_server_not_found))
169-
170-
val dirUrl = baseHttpUrl.newBuilder().apply {
167+
val dirUrl = secureUrl.newBuilder().apply {
171168
path.split('/').filter { it.isNotEmpty() }.forEach { segment ->
172169
addPathSegment(segment)
173170
}

app/src/main/res/values-fa/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,7 @@
592592
<string name="webdav_server_error">خطای سرور</string>
593593
<string name="webdav_connect_failed">اتصال ناموفق بود با کد: %d</string>
594594
<string name="webdav_create_dir_failed">ایجاد یا دسترسی به پوشهٔ پشتیبان ناموفق بود</string>
595+
<string name="webdav_insecure_url">نشانی سرور WebDAV ناامن است. از نشانی https:// استفاده کنید؛ استفاده از http:// اطلاعات ورود و پشتیبان (شامل تمام رمزهای پروفایل) را به‌صورت متن‌ساده منتقل می‌کند.</string>
595596
<string name="global_mode">حالت سراسری</string>
596597
<string name="global_mode_summary">دور زدن همهٔ قواعد مسیریابی و عبور همهٔ ترافیک از گرهٔ فعلی</string>
597598
<string name="mux_preference">Multiplex</string>

app/src/main/res/values-ja/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,7 @@
525525
<string name="webdav_server_error">サーバーエラー</string>
526526
<string name="webdav_connect_failed">接続に失敗しました。コード: %d</string>
527527
<string name="webdav_create_dir_failed">バックアップディレクトリの作成またはアクセスに失敗しました</string>
528+
<string name="webdav_insecure_url">WebDAVサーバーのURLが安全ではありません。https://アドレスを使用してください。http://を使用すると、認証情報とバックアップ(すべてのプロファイルのシークレットを含む)が平文で送信されます。</string>
528529
<string name="global_mode">グローバルモード</string>
529530
<string name="global_mode_summary">すべてのルーティングルールをバイパスし、すべてのトラフィックを現在のノード経由でプロキシします</string>
530531
<string name="mux_preference">マルチプレックス</string>

app/src/main/res/values-ko/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,7 @@
519519
<string name="webdav_server_error">서버 오류</string>
520520
<string name="webdav_connect_failed">연결 실패, 코드: %d</string>
521521
<string name="webdav_create_dir_failed">백업 디렉터리를 만들거나 접근하지 못했습니다</string>
522+
<string name="webdav_insecure_url">안전하지 않은 WebDAV 서버 주소입니다. https:// 주소를 사용하세요. http://를 사용하면 자격 증명과 백업(모든 프로필 비밀 포함)이 평문으로 전송됩니다.</string>
522523
<string name="global_mode">글로벌 모드</string>
523524
<string name="global_mode_summary">모든 라우팅 규칙을 바이패스하고 모든 트래픽을 현재 노드를 통해 프록시합니다</string>
524525
<string name="mux_preference">멀티플렉스</string>

app/src/main/res/values-ru/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,7 @@
599599
<string name="webdav_server_error">Ошибка сервера</string>
600600
<string name="webdav_connect_failed">Не удалось соединиться, код: %d</string>
601601
<string name="webdav_create_dir_failed">Не удалось создать каталог резервных копий или получить к нему доступ</string>
602+
<string name="webdav_insecure_url">Небезопасный URL WebDAV-сервера. Используйте адрес https://; при использовании http:// учётные данные и резервная копия (содержащая все секреты профилей) передаются в открытом виде.</string>
602603
<string name="global_mode">Глобальный режим</string>
603604
<string name="global_mode_summary">Обходить все правила маршрутизации и направлять весь трафик через текущий узел</string>
604605
<string name="backup_in_progress">Резервное копирование уже выполняется</string>

app/src/main/res/values-zh-rCN/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,7 @@
541541
<string name="webdav_server_error">服务器错误</string>
542542
<string name="webdav_connect_failed">连接失败,错误代码:%d</string>
543543
<string name="webdav_create_dir_failed">无法创建或访问备份目录</string>
544+
<string name="webdav_insecure_url">WebDAV 服务器地址不安全。请使用 https:// 地址;使用 http:// 会以明文传输您的凭据和备份(包含所有配置密钥)。</string>
544545
<string name="global_mode">全局模式</string>
545546
<string name="global_mode_summary">绕过所有路由规则,所有流量都通过当前节点</string>
546547
<string name="network_change_reset_connections">当网络发生变化时重置出站连接</string>

app/src/main/res/values-zh-rTW/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,7 @@
543543
<string name="webdav_server_error">伺服器錯誤</string>
544544
<string name="webdav_connect_failed">連線失敗,錯誤程式碼:%d</string>
545545
<string name="webdav_create_dir_failed">無法建立或訪問備份目錄</string>
546+
<string name="webdav_insecure_url">WebDAV 伺服器位址不安全。請使用 https:// 位址;使用 http:// 會以明文傳輸您的憑證與備份(包含所有設定金鑰)。</string>
546547
<string name="global_mode">全域性模式</string>
547548
<string name="global_mode_summary">繞過所有路由規則,所有流量都透過當前節點</string>
548549
<string name="network_change_reset_connections">當網路發生變化時重置出站連線</string>

app/src/main/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,7 @@
628628
<string name="webdav_server_error">Server error</string>
629629
<string name="webdav_connect_failed">Connection failed with code: %d</string>
630630
<string name="webdav_create_dir_failed">Failed to create or access backup directory</string>
631+
<string name="webdav_insecure_url">Insecure WebDAV server URL. Use an https:// address; plain http:// would transmit your credentials and backup (which contains all profile secrets) in cleartext.</string>
631632
<string name="global_mode">Global Mode</string>
632633
<string name="global_mode_summary">Bypass all routing rules and proxy all traffic through current node</string>
633634
<string name="mux_preference">Mulitplex</string>

0 commit comments

Comments
 (0)