Skip to content

fix: sanitize DNS preference inputs#32

Merged
hawkff merged 2 commits into
mainfrom
feat/sanitize-dns-inputs
Jun 19, 2026
Merged

fix: sanitize DNS preference inputs#32
hawkff merged 2 commits into
mainfrom
feat/sanitize-dns-inputs

Conversation

@hawkff

@hawkff hawkff commented Jun 19, 2026

Copy link
Copy Markdown
Owner

Summary

  • strip ISO control characters from direct/remote DNS settings in the UI before persistence
  • re-sanitize DNS entries during sing-box config generation as defense-in-depth

Validation

  • CodeRabbit CLI review: 0 findings

Greptile Summary

This PR adds ISO control-character sanitization to the remoteDns and directDns preference inputs at two layers: stripping control chars in the UI preference listener before persistence, and re-sanitizing each DNS entry during sing-box config generation as a defense-in-depth measure.

  • UI layer (SettingsPreferenceFragment.kt): dnsReloadListener splits the raw input by line, strips ISO control characters per line with filterNot { it.isISOControl() }, and persists the cleaned value via preference.text = sanitizedValue before returning false to discard the raw input from the framework's default persistence path.
  • Config-generation layer (ConfigBuilder.kt): A file-private sanitizeDnsEntry function replaces the previous plain trim() call on each split DNS entry; it adds filterNot { it.isISOControl() } before trimming, ensuring any control characters stored before this fix are neutralized at config build time.

Confidence Score: 5/5

Safe to merge; the sanitization logic is applied correctly at both layers and the value-persistence path through EditTextPreference works as intended.

Both sanitization paths are functionally sound. In the UI layer, preference.text = sanitizedValue calls persistString before return false discards the raw value, so the cleaned value reaches the datastore correctly. In ConfigBuilder the change is a straightforward augmentation of the existing trim() with an additional filterNot { it.isISOControl() }. No data-loss or incorrect-value scenarios were found in the changed code paths.

No files require special attention.

Important Files Changed

Filename Overview
app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt Adds dnsReloadListener with sanitizeDnsPreferenceValue to strip ISO control characters from remoteDns/directDns before persisting; logic is correct — preference.text= persists the sanitized value before returning false to suppress the raw value.
app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt Adds sanitizeDnsEntry (filterNot isISOControl + trim) as a defense-in-depth step during config generation; replaces the previous plain trim() on each DNS entry before passing it to the sing-box config.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant U as User Input
    participant SP as SettingsPreferenceFragment
    participant DS as DataStore
    participant CB as ConfigBuilder

    U->>SP: Enter DNS value (may contain control chars)
    SP->>SP: sanitizeDnsPreferenceValue(rawValue)
    alt "sanitized != raw"
        SP->>DS: "preference.text = sanitizedValue (persist)"
        SP-->>U: return false (discard rawValue)
    else "sanitized == raw"
        SP->>DS: return true (framework persists rawValue)
    end
    SP->>SP: needReload()

    Note over CB,DS: Config generation (defense-in-depth)
    CB->>DS: DataStore.remoteDns / directDns
    CB->>CB: split(newline)
    CB->>CB: sanitizeDnsEntry(entry) per line
    CB->>CB: filterNot isISOControl + trim
    CB->>CB: takeIf isNotBlank and not comment
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant U as User Input
    participant SP as SettingsPreferenceFragment
    participant DS as DataStore
    participant CB as ConfigBuilder

    U->>SP: Enter DNS value (may contain control chars)
    SP->>SP: sanitizeDnsPreferenceValue(rawValue)
    alt "sanitized != raw"
        SP->>DS: "preference.text = sanitizedValue (persist)"
        SP-->>U: return false (discard rawValue)
    else "sanitized == raw"
        SP->>DS: return true (framework persists rawValue)
    end
    SP->>SP: needReload()

    Note over CB,DS: Config generation (defense-in-depth)
    CB->>DS: DataStore.remoteDns / directDns
    CB->>CB: split(newline)
    CB->>CB: sanitizeDnsEntry(entry) per line
    CB->>CB: filterNot isISOControl + trim
    CB->>CB: takeIf isNotBlank and not comment
Loading

Reviews (2): Last reviewed commit: "fix: type DNS sanitizing listeners" | Re-trigger Greptile

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

DNS list inputs for directDns and remoteDns now undergo sanitization that removes ISO control characters and trims whitespace from each line. A helper is added in ConfigBuilder.kt for config generation and separate helpers plus a dedicated preference change listener are added in SettingsPreferenceFragment.kt for the settings UI.

Changes

DNS Input Sanitization

Layer / File(s) Summary
Sanitization helper and directDNS parsing
app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt
Adds a private helper that strips ISO control characters and trims a string; updates directDNS list parsing to run entries through this helper before filtering blank and #-prefixed lines.
UI preference sanitizer and listener wiring
app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt
Adds sanitizeDnsPreferenceValue (per-line strip and trim) and dnsReloadListener (applies sanitized value back to EditTextPreference, calls needReload()); rewires remoteDns and directDns preferences to use dnsReloadListener instead of reloadListener.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 A bunny hops through DNS land,
Trimming control chars with a careful hand,
No sneaky whitespace, no hidden byte,
Each line comes out clean and right,
The config builds pure, the settings shine bright! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: sanitize DNS preference inputs' directly and clearly summarizes the main change: adding sanitization for DNS preference inputs to remove ISO control characters.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The pull request description clearly outlines the changes: sanitizing ISO control characters from DNS settings at two layers (UI and config generation), with implementation details and validation results provided.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment on lines +50 to +62
private fun dnsReloadListener(preference: Preference, newValue: Any?): Boolean {
val rawValue = newValue as? String ?: return reloadListener.onPreferenceChange(preference, newValue)
val sanitizedValue = sanitizeDnsPreferenceValue(rawValue)
if (sanitizedValue != rawValue) {
if (preference is EditTextPreference) {
preference.text = sanitizedValue
}
needReload()
return false
}
needReload()
return true
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 needReload() is called unconditionally in both branches of the if/else, so it can be hoisted before the conditional to remove the duplication.

Suggested change
private fun dnsReloadListener(preference: Preference, newValue: Any?): Boolean {
val rawValue = newValue as? String ?: return reloadListener.onPreferenceChange(preference, newValue)
val sanitizedValue = sanitizeDnsPreferenceValue(rawValue)
if (sanitizedValue != rawValue) {
if (preference is EditTextPreference) {
preference.text = sanitizedValue
}
needReload()
return false
}
needReload()
return true
}
private fun dnsReloadListener(preference: Preference, newValue: Any?): Boolean {
val rawValue = newValue as? String ?: return reloadListener.onPreferenceChange(preference, newValue)
val sanitizedValue = sanitizeDnsPreferenceValue(rawValue)
needReload()
if (sanitizedValue != rawValue) {
if (preference is EditTextPreference) {
preference.text = sanitizedValue
}
return false
}
return true
}

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@hawkff hawkff merged commit 8dc2b50 into main Jun 19, 2026
5 checks passed
@hawkff hawkff deleted the feat/sanitize-dns-inputs branch June 19, 2026 17:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant