feat(mobile): warn on insecure http:// server URLs before native login

SetupScreen now flags a plain http:// address (to anything but loopback) — the
native login POST would send the password in cleartext — and requires an explicit
second tap ("Connect anyway") to proceed, with an override for trusted LAN setups.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
null 2026-07-11 10:51:35 -05:00
parent c1523ece1a
commit 7f7c0ef7b0
1 changed files with 27 additions and 2 deletions

View File

@ -21,9 +21,22 @@ function isValidUrl(url: string): boolean {
} }
} }
// Plain http:// (to anything but loopback) sends the password in cleartext during
// the native login POST. Warn and require an explicit acknowledgement.
function isInsecureUrl(url: string): boolean {
try {
const u = new URL(url);
if (u.protocol !== 'http:') return false;
return !(u.hostname === 'localhost' || u.hostname === '127.0.0.1' || u.hostname === '::1');
} catch {
return false;
}
}
export default function SetupScreen({ onConnect, onLocalMode }: Props) { export default function SetupScreen({ onConnect, onLocalMode }: Props) {
const [url, setUrl] = useState(''); const [url, setUrl] = useState('');
const [error, setError] = useState(''); const [error, setError] = useState('');
const [insecureAck, setInsecureAck] = useState(false);
const [connecting, setConnecting] = useState(false); const [connecting, setConnecting] = useState(false);
function handleConnect() { function handleConnect() {
@ -32,6 +45,12 @@ export default function SetupScreen({ onConnect, onLocalMode }: Props) {
setError('Enter a valid URL, e.g. https://bills.yourdomain.com'); setError('Enter a valid URL, e.g. https://bills.yourdomain.com');
return; return;
} }
// First tap on an insecure URL: warn and require a second, explicit tap.
if (isInsecureUrl(normalized) && !insecureAck) {
setError('');
setInsecureAck(true);
return;
}
setError(''); setError('');
setConnecting(true); setConnecting(true);
onConnect(normalized); onConnect(normalized);
@ -69,11 +88,17 @@ export default function SetupScreen({ onConnect, onLocalMode }: Props) {
spellCheck={false} spellCheck={false}
placeholder="https://bills.yourdomain.com" placeholder="https://bills.yourdomain.com"
value={url} value={url}
onChange={e => { setUrl(e.target.value); setError(''); }} onChange={e => { setUrl(e.target.value); setError(''); setInsecureAck(false); }}
onKeyDown={handleKey} onKeyDown={handleKey}
disabled={connecting} disabled={connecting}
/> />
{error && <p className="form-error">{error}</p>} {error && <p className="form-error">{error}</p>}
{insecureAck && (
<p className="form-error">
This is an insecure <strong>http://</strong> address — your password will be sent
unencrypted. Only continue on a network you trust. Tap again to connect anyway.
</p>
)}
<p className="form-hint"> <p className="form-hint">
The address of your Bill Tracker server. Must be reachable from this device. The address of your Bill Tracker server. Must be reachable from this device.
</p> </p>
@ -84,7 +109,7 @@ export default function SetupScreen({ onConnect, onLocalMode }: Props) {
onClick={handleConnect} onClick={handleConnect}
disabled={connecting || !url.trim()} disabled={connecting || !url.trim()}
> >
{connecting ? 'Connecting…' : 'Continue'} {connecting ? 'Connecting…' : insecureAck ? 'Connect anyway' : 'Continue'}
</button> </button>
<div className="divider"> <div className="divider">