mirror of
https://github.com/NetworkCats/rustyip.git
synced 2026-11-13 16:17:46 +00:00
feat: reject non-public IP queries on both backend and frontend
This commit is contained in:
@@ -177,6 +177,9 @@ msgstr "لم يتم العثور على عنوان IP في قاعدة البيا
|
||||
msgid "Invalid IP address"
|
||||
msgstr "عنوان IP غير صالح"
|
||||
|
||||
msgid "Only public IP addresses can be queried"
|
||||
msgstr "يمكن الاستعلام عن عناوين IP العامة فقط"
|
||||
|
||||
msgid "Missing client IP address"
|
||||
msgstr "عنوان IP للعميل مفقود"
|
||||
|
||||
|
||||
@@ -177,6 +177,9 @@ msgstr "IP nicht in der Datenbank gefunden"
|
||||
msgid "Invalid IP address"
|
||||
msgstr "Ungueltige IP-Adresse"
|
||||
|
||||
msgid "Only public IP addresses can be queried"
|
||||
msgstr "Es koennen nur oeffentliche IP-Adressen abgefragt werden"
|
||||
|
||||
msgid "Missing client IP address"
|
||||
msgstr "Fehlende Client-IP-Adresse"
|
||||
|
||||
|
||||
@@ -177,6 +177,9 @@ msgstr "IP not found in database"
|
||||
msgid "Invalid IP address"
|
||||
msgstr "Invalid IP address"
|
||||
|
||||
msgid "Only public IP addresses can be queried"
|
||||
msgstr "Only public IP addresses can be queried"
|
||||
|
||||
msgid "Missing client IP address"
|
||||
msgstr "Missing client IP address"
|
||||
|
||||
|
||||
@@ -177,6 +177,9 @@ msgstr "IP no encontrada en la base de datos"
|
||||
msgid "Invalid IP address"
|
||||
msgstr "Direccion IP no valida"
|
||||
|
||||
msgid "Only public IP addresses can be queried"
|
||||
msgstr "Solo se pueden consultar direcciones IP publicas"
|
||||
|
||||
msgid "Missing client IP address"
|
||||
msgstr "Falta la direccion IP del cliente"
|
||||
|
||||
|
||||
@@ -177,6 +177,9 @@ msgstr "IP introuvable dans la base de donnees"
|
||||
msgid "Invalid IP address"
|
||||
msgstr "Adresse IP invalide"
|
||||
|
||||
msgid "Only public IP addresses can be queried"
|
||||
msgstr "Seules les adresses IP publiques peuvent etre interrogees"
|
||||
|
||||
msgid "Missing client IP address"
|
||||
msgstr "Adresse IP du client manquante"
|
||||
|
||||
|
||||
@@ -177,6 +177,9 @@ msgstr "IP tidak ditemukan dalam basis data"
|
||||
msgid "Invalid IP address"
|
||||
msgstr "Alamat IP tidak valid"
|
||||
|
||||
msgid "Only public IP addresses can be queried"
|
||||
msgstr "Hanya alamat IP publik yang dapat dicari"
|
||||
|
||||
msgid "Missing client IP address"
|
||||
msgstr "Alamat IP klien tidak ditemukan"
|
||||
|
||||
|
||||
@@ -177,6 +177,9 @@ msgstr "IP non trovato nel database"
|
||||
msgid "Invalid IP address"
|
||||
msgstr "Indirizzo IP non valido"
|
||||
|
||||
msgid "Only public IP addresses can be queried"
|
||||
msgstr "Possono essere interrogati solo indirizzi IP pubblici"
|
||||
|
||||
msgid "Missing client IP address"
|
||||
msgstr "Indirizzo IP del client mancante"
|
||||
|
||||
|
||||
@@ -177,6 +177,9 @@ msgstr "IPがデータベースに見つかりません"
|
||||
msgid "Invalid IP address"
|
||||
msgstr "無効なIPアドレス"
|
||||
|
||||
msgid "Only public IP addresses can be queried"
|
||||
msgstr "パブリックIPアドレスのみ照会できます"
|
||||
|
||||
msgid "Missing client IP address"
|
||||
msgstr "クライアントIPアドレスがありません"
|
||||
|
||||
|
||||
@@ -177,6 +177,9 @@ msgstr "데이터베이스에서 IP를 찾을 수 없습니다"
|
||||
msgid "Invalid IP address"
|
||||
msgstr "유효하지 않은 IP 주소"
|
||||
|
||||
msgid "Only public IP addresses can be queried"
|
||||
msgstr "공용 IP 주소만 조회할 수 있습니다"
|
||||
|
||||
msgid "Missing client IP address"
|
||||
msgstr "클라이언트 IP 주소가 없습니다"
|
||||
|
||||
|
||||
@@ -177,6 +177,9 @@ msgstr "IP niet gevonden in database"
|
||||
msgid "Invalid IP address"
|
||||
msgstr "Ongeldig IP-adres"
|
||||
|
||||
msgid "Only public IP addresses can be queried"
|
||||
msgstr "Alleen openbare IP-adressen kunnen worden opgezocht"
|
||||
|
||||
msgid "Missing client IP address"
|
||||
msgstr "Ontbrekend client-IP-adres"
|
||||
|
||||
|
||||
@@ -177,6 +177,9 @@ msgstr "IP não encontrado no banco de dados"
|
||||
msgid "Invalid IP address"
|
||||
msgstr "Endereço IP inválido"
|
||||
|
||||
msgid "Only public IP addresses can be queried"
|
||||
msgstr "Apenas enderecos IP publicos podem ser consultados"
|
||||
|
||||
msgid "Missing client IP address"
|
||||
msgstr "Endereço IP do cliente ausente"
|
||||
|
||||
|
||||
@@ -177,6 +177,9 @@ msgstr "IP не найден в базе данных"
|
||||
msgid "Invalid IP address"
|
||||
msgstr "Недействительный IP-адрес"
|
||||
|
||||
msgid "Only public IP addresses can be queried"
|
||||
msgstr "Можно запрашивать только публичные IP-адреса"
|
||||
|
||||
msgid "Missing client IP address"
|
||||
msgstr "Отсутствует IP-адрес клиента"
|
||||
|
||||
|
||||
@@ -177,6 +177,9 @@ msgstr "在数据库中未找到该 IP"
|
||||
msgid "Invalid IP address"
|
||||
msgstr "无效的 IP 地址"
|
||||
|
||||
msgid "Only public IP addresses can be queried"
|
||||
msgstr "仅可查询公网 IP 地址"
|
||||
|
||||
msgid "Missing client IP address"
|
||||
msgstr "缺少客户端 IP 地址"
|
||||
|
||||
|
||||
@@ -177,6 +177,9 @@ msgstr "在資料庫中找不到此 IP"
|
||||
msgid "Invalid IP address"
|
||||
msgstr "無效的 IP 位址"
|
||||
|
||||
msgid "Only public IP addresses can be queried"
|
||||
msgstr "僅可查詢公網 IP 位址"
|
||||
|
||||
msgid "Missing client IP address"
|
||||
msgstr "缺少用戶端 IP 位址"
|
||||
|
||||
|
||||
+13
-1
@@ -4,6 +4,7 @@ use axum::response::{IntoResponse, Response};
|
||||
pub enum AppError {
|
||||
IpNotFound,
|
||||
InvalidIp,
|
||||
NonPublicIp,
|
||||
MissingClientIp,
|
||||
DbLookupFailed,
|
||||
TemplateRenderFailed,
|
||||
@@ -13,7 +14,7 @@ impl AppError {
|
||||
pub fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
Self::IpNotFound => StatusCode::NOT_FOUND,
|
||||
Self::InvalidIp => StatusCode::BAD_REQUEST,
|
||||
Self::InvalidIp | Self::NonPublicIp => StatusCode::BAD_REQUEST,
|
||||
Self::MissingClientIp => StatusCode::BAD_REQUEST,
|
||||
Self::DbLookupFailed => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Self::TemplateRenderFailed => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
@@ -24,6 +25,7 @@ impl AppError {
|
||||
match self {
|
||||
Self::IpNotFound => "IP not found in database",
|
||||
Self::InvalidIp => "Invalid IP address",
|
||||
Self::NonPublicIp => "Only public IP addresses can be queried",
|
||||
Self::MissingClientIp => "Missing client IP address",
|
||||
Self::DbLookupFailed => "Database lookup failed",
|
||||
Self::TemplateRenderFailed => "Template render failed",
|
||||
@@ -74,6 +76,16 @@ mod tests {
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn non_public_ip_returns_400() {
|
||||
check_error_response(
|
||||
AppError::NonPublicIp,
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Only public IP addresses can be queried",
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn missing_client_ip_returns_400() {
|
||||
check_error_response(
|
||||
|
||||
+19
-4
@@ -10,6 +10,7 @@ use crate::cli_detect::is_cli_user_agent;
|
||||
use crate::db::{self, SharedDb};
|
||||
use crate::error::AppError;
|
||||
use crate::i18n::{self, Locale};
|
||||
use crate::ip_validate;
|
||||
use crate::models::{IpInfo, get_localized_name};
|
||||
use crate::static_assets;
|
||||
|
||||
@@ -50,6 +51,8 @@ struct IndexTemplate {
|
||||
t_description: &'static str,
|
||||
t_search_label: &'static str,
|
||||
t_search_placeholder: &'static str,
|
||||
t_invalid_ip: &'static str,
|
||||
t_non_public_ip: &'static str,
|
||||
t_asn: &'static str,
|
||||
t_org: &'static str,
|
||||
t_country: &'static str,
|
||||
@@ -113,6 +116,8 @@ struct ErrorAlertTemplate {
|
||||
t_error: &'static str,
|
||||
t_search_label: &'static str,
|
||||
t_search_placeholder: &'static str,
|
||||
t_invalid_ip: &'static str,
|
||||
t_non_public_ip: &'static str,
|
||||
t_footer_license: &'static str,
|
||||
t_footer_source: &'static str,
|
||||
t_footer_db: &'static str,
|
||||
@@ -139,7 +144,11 @@ fn resolve_ip(headers: &HeaderMap, query: &IpQuery, dev_mode: bool) -> Result<Ip
|
||||
if let Some(ref ip_str) = query.ip {
|
||||
let trimmed = ip_str.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return trimmed.parse().map_err(|_| AppError::InvalidIp);
|
||||
let ip: IpAddr = trimmed.parse().map_err(|_| AppError::InvalidIp)?;
|
||||
if !ip_validate::is_global_ip(ip) {
|
||||
return Err(AppError::NonPublicIp);
|
||||
}
|
||||
return Ok(ip);
|
||||
}
|
||||
}
|
||||
extract_client_ip(headers, dev_mode)
|
||||
@@ -211,6 +220,8 @@ fn build_translations(locale: Locale) -> Translations {
|
||||
),
|
||||
t_search_label: i18n::translate(locale, "Search IP address"),
|
||||
t_search_placeholder: i18n::translate(locale, "Type to search IP data"),
|
||||
t_invalid_ip: i18n::translate(locale, "Invalid IP address"),
|
||||
t_non_public_ip: i18n::translate(locale, "Only public IP addresses can be queried"),
|
||||
t_asn: i18n::translate(locale, "ASN"),
|
||||
t_org: i18n::translate(locale, "Org"),
|
||||
t_country: i18n::translate(locale, "Country"),
|
||||
@@ -319,6 +330,8 @@ struct Translations {
|
||||
t_description: &'static str,
|
||||
t_search_label: &'static str,
|
||||
t_search_placeholder: &'static str,
|
||||
t_invalid_ip: &'static str,
|
||||
t_non_public_ip: &'static str,
|
||||
t_asn: &'static str,
|
||||
t_org: &'static str,
|
||||
t_country: &'static str,
|
||||
@@ -384,6 +397,8 @@ fn render_error_alert(state: &AppState, locale: Locale, query: &str, error: &App
|
||||
t_error: t.t_error,
|
||||
t_search_label: t.t_search_label,
|
||||
t_search_placeholder: t.t_search_placeholder,
|
||||
t_invalid_ip: t.t_invalid_ip,
|
||||
t_non_public_ip: t.t_non_public_ip,
|
||||
t_footer_license: t.t_footer_license,
|
||||
t_footer_source: t.t_footer_source,
|
||||
t_footer_db: t.t_footer_db,
|
||||
@@ -556,6 +571,8 @@ pub async fn root(
|
||||
t_description: t.t_description,
|
||||
t_search_label: t.t_search_label,
|
||||
t_search_placeholder: t.t_search_placeholder,
|
||||
t_invalid_ip: t.t_invalid_ip,
|
||||
t_non_public_ip: t.t_non_public_ip,
|
||||
t_asn: t.t_asn,
|
||||
t_org: t.t_org,
|
||||
t_country: t.t_country,
|
||||
@@ -780,9 +797,7 @@ pub async fn sitemap_xml(State(state): State<AppState>) -> Response {
|
||||
|
||||
for locale in Locale::ALL {
|
||||
let tag = locale.tag();
|
||||
urls.push_str(&format!(
|
||||
" <url>\n <loc>https://{domain}/{tag}</loc>\n"
|
||||
));
|
||||
urls.push_str(&format!(" <url>\n <loc>https://{domain}/{tag}</loc>\n"));
|
||||
for alt in Locale::ALL {
|
||||
let alt_tag = alt.tag();
|
||||
urls.push_str(&format!(
|
||||
|
||||
@@ -0,0 +1,513 @@
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
|
||||
/// Returns `true` if the given IPv4 address is a globally routable public address.
|
||||
///
|
||||
/// Rejects: private (RFC 1918), loopback, link-local, broadcast, documentation,
|
||||
/// benchmarking, reserved, shared (CGN), protocol assignment, and all other
|
||||
/// non-globally-routable ranges.
|
||||
fn is_global_ipv4(ip: Ipv4Addr) -> bool {
|
||||
let octets = ip.octets();
|
||||
let first = octets[0];
|
||||
let second = octets[1];
|
||||
|
||||
// 0.0.0.0/8 - "This" network (RFC 1122)
|
||||
if first == 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 10.0.0.0/8 - Private (RFC 1918)
|
||||
if first == 10 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 100.64.0.0/10 - Shared address space / CGN (RFC 6598)
|
||||
if first == 100 && (second & 0xC0) == 64 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 127.0.0.0/8 - Loopback (RFC 1122)
|
||||
if first == 127 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 169.254.0.0/16 - Link-local (RFC 3927)
|
||||
if first == 169 && second == 254 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 172.16.0.0/12 - Private (RFC 1918)
|
||||
if first == 172 && (second & 0xF0) == 16 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 192.0.0.0/24 - IETF protocol assignments (RFC 6890)
|
||||
if first == 192 && second == 0 && octets[2] == 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 192.0.2.0/24 - Documentation TEST-NET-1 (RFC 5737)
|
||||
if first == 192 && second == 0 && octets[2] == 2 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 192.88.99.0/24 - 6to4 relay anycast (RFC 7526, deprecated)
|
||||
if first == 192 && second == 88 && octets[2] == 99 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 192.168.0.0/16 - Private (RFC 1918)
|
||||
if first == 192 && second == 168 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 198.18.0.0/15 - Benchmarking (RFC 2544)
|
||||
if first == 198 && (second == 18 || second == 19) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 198.51.100.0/24 - Documentation TEST-NET-2 (RFC 5737)
|
||||
if first == 198 && second == 51 && octets[2] == 100 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 203.0.113.0/24 - Documentation TEST-NET-3 (RFC 5737)
|
||||
if first == 203 && second == 0 && octets[2] == 113 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 224.0.0.0/4 - Multicast (RFC 5771)
|
||||
// 240.0.0.0/4 - Reserved for future use (RFC 1112) + 255.255.255.255 broadcast
|
||||
if first >= 224 {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Returns `true` if the given IPv6 address is a globally routable public address.
|
||||
///
|
||||
/// Rejects: unspecified (::), loopback (::1), IPv4-mapped, IPv4-compatible,
|
||||
/// link-local, site-local (deprecated), unique local (ULA), multicast,
|
||||
/// documentation, discard, Teredo, 6to4, ORCHID, and all other
|
||||
/// non-globally-routable ranges.
|
||||
fn is_global_ipv6(ip: Ipv6Addr) -> bool {
|
||||
// Unspecified address (::)
|
||||
if ip.is_unspecified() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Loopback (::1)
|
||||
if ip.is_loopback() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let segments = ip.segments();
|
||||
let first = segments[0];
|
||||
|
||||
// IPv4-mapped (::ffff:0:0/96) - check the embedded IPv4 part
|
||||
if first == 0
|
||||
&& segments[1] == 0
|
||||
&& segments[2] == 0
|
||||
&& segments[3] == 0
|
||||
&& segments[4] == 0
|
||||
&& segments[5] == 0xFFFF
|
||||
{
|
||||
let ipv4 = Ipv4Addr::new(
|
||||
(segments[6] >> 8) as u8,
|
||||
segments[6] as u8,
|
||||
(segments[7] >> 8) as u8,
|
||||
segments[7] as u8,
|
||||
);
|
||||
return is_global_ipv4(ipv4);
|
||||
}
|
||||
|
||||
// IPv4-compatible (deprecated, ::/96 excluding :: and ::1)
|
||||
if first == 0
|
||||
&& segments[1] == 0
|
||||
&& segments[2] == 0
|
||||
&& segments[3] == 0
|
||||
&& segments[4] == 0
|
||||
&& segments[5] == 0
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// fe80::/10 - Link-local
|
||||
if (first & 0xFFC0) == 0xFE80 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// fec0::/10 - Site-local (deprecated, RFC 3879)
|
||||
if (first & 0xFFC0) == 0xFEC0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// fc00::/7 - Unique local address (ULA, RFC 4193)
|
||||
if (first & 0xFE00) == 0xFC00 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ff00::/8 - Multicast
|
||||
if (first & 0xFF00) == 0xFF00 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 100::/64 - Discard prefix (RFC 6666)
|
||||
if first == 0x0100 && segments[1] == 0 && segments[2] == 0 && segments[3] == 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2001:db8::/32 - Documentation (RFC 3849)
|
||||
if first == 0x2001 && segments[1] == 0x0DB8 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2001::/23 - IETF protocol assignments (RFC 2928)
|
||||
// This includes 2001::/32 (Teredo) and 2001:10::/28 (ORCHID v2)
|
||||
// but also encompasses globally routable 2001::/32 allocations.
|
||||
// We specifically block Teredo (2001:0000::/32) which tunnels arbitrary IPv4.
|
||||
if first == 0x2001 && segments[1] == 0x0000 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2002::/16 - 6to4 (RFC 3056) - check embedded IPv4 for public-ness
|
||||
if first == 0x2002 {
|
||||
let ipv4 = Ipv4Addr::new(
|
||||
(segments[1] >> 8) as u8,
|
||||
segments[1] as u8,
|
||||
(segments[2] >> 8) as u8,
|
||||
segments[2] as u8,
|
||||
);
|
||||
return is_global_ipv4(ipv4);
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Returns `true` if the IP address is a globally routable public address
|
||||
/// suitable for geolocation lookup. Works for both IPv4 and IPv6.
|
||||
pub fn is_global_ip(ip: IpAddr) -> bool {
|
||||
match ip {
|
||||
IpAddr::V4(v4) => is_global_ipv4(v4),
|
||||
IpAddr::V6(v6) => is_global_ipv6(v6),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// --- IPv4 tests ---
|
||||
|
||||
#[test]
|
||||
fn public_ipv4_accepted() {
|
||||
let public_ips = [
|
||||
"1.1.1.1",
|
||||
"8.8.8.8",
|
||||
"45.77.77.77",
|
||||
"104.16.132.229",
|
||||
"223.255.255.255",
|
||||
"100.0.0.1",
|
||||
"100.63.255.255",
|
||||
"100.128.0.0",
|
||||
"198.17.255.255",
|
||||
"198.20.0.0",
|
||||
];
|
||||
for s in public_ips {
|
||||
let ip: IpAddr = s.parse().unwrap();
|
||||
assert!(is_global_ip(ip), "{s} should be accepted as public");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn this_network_rejected() {
|
||||
let ips = ["0.0.0.0", "0.0.0.1", "0.255.255.255"];
|
||||
for s in ips {
|
||||
let ip: IpAddr = s.parse().unwrap();
|
||||
assert!(!is_global_ip(ip), "{s} should be rejected (this network)");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private_rfc1918_rejected() {
|
||||
let ips = [
|
||||
"10.0.0.0",
|
||||
"10.0.0.1",
|
||||
"10.255.255.255",
|
||||
"172.16.0.0",
|
||||
"172.16.0.1",
|
||||
"172.31.255.255",
|
||||
"192.168.0.0",
|
||||
"192.168.0.1",
|
||||
"192.168.255.255",
|
||||
];
|
||||
for s in ips {
|
||||
let ip: IpAddr = s.parse().unwrap();
|
||||
assert!(!is_global_ip(ip), "{s} should be rejected (RFC 1918)");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private_boundary_not_rejected() {
|
||||
let ips = ["172.15.255.255", "172.32.0.0"];
|
||||
for s in ips {
|
||||
let ip: IpAddr = s.parse().unwrap();
|
||||
assert!(
|
||||
is_global_ip(ip),
|
||||
"{s} should be accepted (outside RFC 1918)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loopback_rejected() {
|
||||
let ips = ["127.0.0.0", "127.0.0.1", "127.255.255.255"];
|
||||
for s in ips {
|
||||
let ip: IpAddr = s.parse().unwrap();
|
||||
assert!(!is_global_ip(ip), "{s} should be rejected (loopback)");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn link_local_rejected() {
|
||||
let ips = ["169.254.0.0", "169.254.0.1", "169.254.255.255"];
|
||||
for s in ips {
|
||||
let ip: IpAddr = s.parse().unwrap();
|
||||
assert!(!is_global_ip(ip), "{s} should be rejected (link-local)");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cgn_shared_rejected() {
|
||||
let ips = ["100.64.0.0", "100.64.0.1", "100.127.255.255"];
|
||||
for s in ips {
|
||||
let ip: IpAddr = s.parse().unwrap();
|
||||
assert!(!is_global_ip(ip), "{s} should be rejected (CGN/shared)");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn documentation_nets_rejected() {
|
||||
let ips = [
|
||||
"192.0.2.0",
|
||||
"192.0.2.1",
|
||||
"192.0.2.255",
|
||||
"198.51.100.0",
|
||||
"198.51.100.255",
|
||||
"203.0.113.0",
|
||||
"203.0.113.255",
|
||||
];
|
||||
for s in ips {
|
||||
let ip: IpAddr = s.parse().unwrap();
|
||||
assert!(!is_global_ip(ip), "{s} should be rejected (documentation)");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn benchmarking_rejected() {
|
||||
let ips = ["198.18.0.0", "198.18.0.1", "198.19.255.255"];
|
||||
for s in ips {
|
||||
let ip: IpAddr = s.parse().unwrap();
|
||||
assert!(!is_global_ip(ip), "{s} should be rejected (benchmarking)");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ietf_protocol_rejected() {
|
||||
let ips = ["192.0.0.0", "192.0.0.1", "192.0.0.255"];
|
||||
for s in ips {
|
||||
let ip: IpAddr = s.parse().unwrap();
|
||||
assert!(!is_global_ip(ip), "{s} should be rejected (IETF protocol)");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relay_6to4_rejected() {
|
||||
let ips = ["192.88.99.0", "192.88.99.1", "192.88.99.255"];
|
||||
for s in ips {
|
||||
let ip: IpAddr = s.parse().unwrap();
|
||||
assert!(!is_global_ip(ip), "{s} should be rejected (6to4 relay)");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multicast_rejected() {
|
||||
let ips = ["224.0.0.0", "224.0.0.1", "239.255.255.255"];
|
||||
for s in ips {
|
||||
let ip: IpAddr = s.parse().unwrap();
|
||||
assert!(!is_global_ip(ip), "{s} should be rejected (multicast)");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reserved_and_broadcast_rejected() {
|
||||
let ips = [
|
||||
"240.0.0.0",
|
||||
"240.0.0.1",
|
||||
"255.255.255.254",
|
||||
"255.255.255.255",
|
||||
];
|
||||
for s in ips {
|
||||
let ip: IpAddr = s.parse().unwrap();
|
||||
assert!(
|
||||
!is_global_ip(ip),
|
||||
"{s} should be rejected (reserved/broadcast)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- IPv6 tests ---
|
||||
|
||||
#[test]
|
||||
fn public_ipv6_accepted() {
|
||||
let public_ips = [
|
||||
"2606:4700:4700::1111",
|
||||
"2001:4860:4860::8888",
|
||||
"2400:cb00:2048:1::c629:d7a2",
|
||||
];
|
||||
for s in public_ips {
|
||||
let ip: IpAddr = s.parse().unwrap();
|
||||
assert!(is_global_ip(ip), "{s} should be accepted as public");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ipv6_unspecified_rejected() {
|
||||
let ip: IpAddr = "::".parse().unwrap();
|
||||
assert!(!is_global_ip(ip), ":: should be rejected (unspecified)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ipv6_loopback_rejected() {
|
||||
let ip: IpAddr = "::1".parse().unwrap();
|
||||
assert!(!is_global_ip(ip), "::1 should be rejected (loopback)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ipv6_link_local_rejected() {
|
||||
let ips = ["fe80::", "fe80::1", "fe80::ffff:ffff:ffff:ffff", "febf::1"];
|
||||
for s in ips {
|
||||
let ip: IpAddr = s.parse().unwrap();
|
||||
assert!(!is_global_ip(ip), "{s} should be rejected (link-local)");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ipv6_site_local_rejected() {
|
||||
let ips = ["fec0::", "fec0::1", "feff::1"];
|
||||
for s in ips {
|
||||
let ip: IpAddr = s.parse().unwrap();
|
||||
assert!(!is_global_ip(ip), "{s} should be rejected (site-local)");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ipv6_ula_rejected() {
|
||||
let ips = [
|
||||
"fc00::",
|
||||
"fc00::1",
|
||||
"fd00::1",
|
||||
"fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
|
||||
];
|
||||
for s in ips {
|
||||
let ip: IpAddr = s.parse().unwrap();
|
||||
assert!(!is_global_ip(ip), "{s} should be rejected (ULA)");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ipv6_multicast_rejected() {
|
||||
let ips = ["ff00::", "ff02::1", "ff0e::1"];
|
||||
for s in ips {
|
||||
let ip: IpAddr = s.parse().unwrap();
|
||||
assert!(!is_global_ip(ip), "{s} should be rejected (multicast)");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ipv6_documentation_rejected() {
|
||||
let ips = [
|
||||
"2001:db8::",
|
||||
"2001:db8::1",
|
||||
"2001:db8:ffff:ffff:ffff:ffff:ffff:ffff",
|
||||
];
|
||||
for s in ips {
|
||||
let ip: IpAddr = s.parse().unwrap();
|
||||
assert!(!is_global_ip(ip), "{s} should be rejected (documentation)");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ipv6_teredo_rejected() {
|
||||
let ips = [
|
||||
"2001::",
|
||||
"2001::1",
|
||||
"2001:0000:ffff:ffff:ffff:ffff:ffff:ffff",
|
||||
];
|
||||
for s in ips {
|
||||
let ip: IpAddr = s.parse().unwrap();
|
||||
assert!(!is_global_ip(ip), "{s} should be rejected (Teredo)");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ipv6_discard_rejected() {
|
||||
let ips = ["100::", "100::1", "100::ffff:ffff:ffff:ffff"];
|
||||
for s in ips {
|
||||
let ip: IpAddr = s.parse().unwrap();
|
||||
assert!(!is_global_ip(ip), "{s} should be rejected (discard)");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ipv6_ipv4_mapped_private_rejected() {
|
||||
let ips = [
|
||||
"::ffff:127.0.0.1",
|
||||
"::ffff:10.0.0.1",
|
||||
"::ffff:192.168.1.1",
|
||||
"::ffff:0.0.0.0",
|
||||
];
|
||||
for s in ips {
|
||||
let ip: IpAddr = s.parse().unwrap();
|
||||
assert!(!is_global_ip(ip), "{s} should be rejected (mapped private)");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ipv6_ipv4_mapped_public_accepted() {
|
||||
let ips = ["::ffff:1.1.1.1", "::ffff:8.8.8.8"];
|
||||
for s in ips {
|
||||
let ip: IpAddr = s.parse().unwrap();
|
||||
assert!(is_global_ip(ip), "{s} should be accepted (mapped public)");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ipv6_ipv4_compatible_rejected() {
|
||||
let ips = ["::1.1.1.1", "::8.8.8.8"];
|
||||
for s in ips {
|
||||
let ip: IpAddr = s.parse().unwrap();
|
||||
assert!(
|
||||
!is_global_ip(ip),
|
||||
"{s} should be rejected (IPv4-compatible)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ipv6_6to4_with_private_ipv4_rejected() {
|
||||
// 2002:0a00:0001:: embeds 10.0.0.1
|
||||
let ip: IpAddr = "2002:0a00:0001::".parse().unwrap();
|
||||
assert!(
|
||||
!is_global_ip(ip),
|
||||
"6to4 with private IPv4 should be rejected"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ipv6_6to4_with_public_ipv4_accepted() {
|
||||
// 2002:0101:0101:: embeds 1.1.1.1
|
||||
let ip: IpAddr = "2002:0101:0101::".parse().unwrap();
|
||||
assert!(is_global_ip(ip), "6to4 with public IPv4 should be accepted");
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ pub mod db;
|
||||
pub mod error;
|
||||
pub mod handlers;
|
||||
pub mod i18n;
|
||||
pub mod ip_validate;
|
||||
pub mod middleware;
|
||||
pub mod models;
|
||||
pub mod routes;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
use axum::Router;
|
||||
use axum::middleware;
|
||||
use axum::routing::get;
|
||||
use axum::Router;
|
||||
|
||||
use crate::handlers::{self, AppState};
|
||||
use crate::middleware::security_headers;
|
||||
|
||||
@@ -118,6 +118,20 @@ main {
|
||||
&::placeholder {
|
||||
color: var(--color-label-tertiary);
|
||||
}
|
||||
|
||||
&[aria-invalid="true"] {
|
||||
border-color: var(--color-red);
|
||||
|
||||
&:focus-visible {
|
||||
outline-color: var(--color-red);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-error {
|
||||
margin-block-start: 6px;
|
||||
font-size: 13px;
|
||||
color: var(--color-red);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+144
-1
@@ -1,4 +1,6 @@
|
||||
<form method="GET" action="/{{ lang_tag }}" class="search-form" role="search">
|
||||
<form method="GET" action="/{{ lang_tag }}" class="search-form" role="search"
|
||||
data-msg-invalid="{{ t_invalid_ip }}"
|
||||
data-msg-non-public="{{ t_non_public_ip }}">
|
||||
<div class="search-input-wrap">
|
||||
<label for="ip-search" class="sr-only">{{ t_search_label }}</label>
|
||||
<i class="icon-magnifier search-icon" aria-hidden="true"></i>
|
||||
@@ -13,5 +15,146 @@
|
||||
spellcheck="false"
|
||||
{% if autofocus %}autofocus{% endif %}
|
||||
>
|
||||
<p class="search-error" id="ip-error" role="alert" aria-live="polite" hidden></p>
|
||||
</div>
|
||||
</form>
|
||||
<script>
|
||||
(function(){
|
||||
"use strict";
|
||||
var form=document.querySelector(".search-form");
|
||||
if(!form)return;
|
||||
var input=form.querySelector("input[name=ip]");
|
||||
var error=document.getElementById("ip-error");
|
||||
var msgInvalid=form.getAttribute("data-msg-invalid");
|
||||
var msgNonPublic=form.getAttribute("data-msg-non-public");
|
||||
|
||||
function parseIPv4(s){
|
||||
var p=s.split(".");
|
||||
if(p.length!==4)return null;
|
||||
var o=[];
|
||||
for(var i=0;i<4;i++){
|
||||
if(!/^\d{1,3}$/.test(p[i]))return null;
|
||||
var n=parseInt(p[i],10);
|
||||
if(n>255||p[i]!==String(n))return null;
|
||||
o.push(n);
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
function isGlobalIPv4(o){
|
||||
var a=o[0],b=o[1],c=o[2];
|
||||
if(a===0)return false;
|
||||
if(a===10)return false;
|
||||
if(a===100&&(b&192)===64)return false;
|
||||
if(a===127)return false;
|
||||
if(a===169&&b===254)return false;
|
||||
if(a===172&&(b&240)===16)return false;
|
||||
if(a===192&&b===0&&c===0)return false;
|
||||
if(a===192&&b===0&&c===2)return false;
|
||||
if(a===192&&b===88&&c===99)return false;
|
||||
if(a===192&&b===168)return false;
|
||||
if(a===198&&(b===18||b===19))return false;
|
||||
if(a===198&&b===51&&c===100)return false;
|
||||
if(a===203&&b===0&&c===113)return false;
|
||||
if(a>=224&&a<=239)return false;
|
||||
if(a>=240)return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function expandIPv6(s){
|
||||
s=s.replace(/^\[|\]$/g,"");
|
||||
var halves=s.split("::");
|
||||
if(halves.length>2)return null;
|
||||
function parseHalf(h){
|
||||
if(h==="")return[];
|
||||
var parts=h.split(":");
|
||||
var last=parts[parts.length-1];
|
||||
if(last.indexOf(".")!==-1){
|
||||
var v4=parseIPv4(last);
|
||||
if(!v4)return null;
|
||||
parts.pop();
|
||||
parts.push(((v4[0]<<8)|v4[1]).toString(16));
|
||||
parts.push(((v4[2]<<8)|v4[3]).toString(16));
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
var left=parseHalf(halves[0]);
|
||||
if(left===null)return null;
|
||||
var right=halves.length===2?parseHalf(halves[1]):[];
|
||||
if(right===null)return null;
|
||||
if(halves.length===1&&left.length!==8)return null;
|
||||
if(halves.length===2){
|
||||
var fill=8-left.length-right.length;
|
||||
if(fill<0)return null;
|
||||
var mid=[];
|
||||
for(var i=0;i<fill;i++)mid.push("0");
|
||||
left=left.concat(mid,right);
|
||||
}
|
||||
if(left.length!==8)return null;
|
||||
var seg=[];
|
||||
for(var j=0;j<8;j++){
|
||||
if(!/^[0-9a-fA-F]{1,4}$/.test(left[j]))return null;
|
||||
seg.push(parseInt(left[j],16));
|
||||
}
|
||||
return seg;
|
||||
}
|
||||
|
||||
function isGlobalIPv6(seg){
|
||||
if(seg[0]===0&&seg[1]===0&&seg[2]===0&&seg[3]===0&&seg[4]===0&&seg[5]===0&&seg[6]===0&&seg[7]===0)return false;
|
||||
if(seg[0]===0&&seg[1]===0&&seg[2]===0&&seg[3]===0&&seg[4]===0&&seg[5]===0&&seg[6]===0&&seg[7]===1)return false;
|
||||
if(seg[0]===0&&seg[1]===0&&seg[2]===0&&seg[3]===0&&seg[4]===0&&seg[5]===0xFFFF){
|
||||
return isGlobalIPv4([seg[6]>>8,seg[6]&255,seg[7]>>8,seg[7]&255]);
|
||||
}
|
||||
if(seg[0]===0&&seg[1]===0&&seg[2]===0&&seg[3]===0&&seg[4]===0&&seg[5]===0)return false;
|
||||
var f=seg[0];
|
||||
if((f&0xFFC0)===0xFE80)return false;
|
||||
if((f&0xFFC0)===0xFEC0)return false;
|
||||
if((f&0xFE00)===0xFC00)return false;
|
||||
if((f&0xFF00)===0xFF00)return false;
|
||||
if(f===0x0100&&seg[1]===0&&seg[2]===0&&seg[3]===0)return false;
|
||||
if(f===0x2001&&seg[1]===0x0DB8)return false;
|
||||
if(f===0x2001&&seg[1]===0x0000)return false;
|
||||
if(f===0x2002){
|
||||
return isGlobalIPv4([seg[1]>>8,seg[1]&255,seg[2]>>8,seg[2]&255]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function validate(val){
|
||||
var s=val.trim();
|
||||
if(s==="")return null;
|
||||
var v4=parseIPv4(s);
|
||||
if(v4)return isGlobalIPv4(v4)?null:msgNonPublic;
|
||||
var v6=expandIPv6(s);
|
||||
if(v6)return isGlobalIPv6(v6)?null:msgNonPublic;
|
||||
return msgInvalid;
|
||||
}
|
||||
|
||||
function showError(msg){
|
||||
if(msg){
|
||||
error.textContent=msg;
|
||||
error.hidden=false;
|
||||
input.setAttribute("aria-invalid","true");
|
||||
input.setAttribute("aria-describedby","ip-error");
|
||||
}else{
|
||||
error.textContent="";
|
||||
error.hidden=true;
|
||||
input.removeAttribute("aria-invalid");
|
||||
input.removeAttribute("aria-describedby");
|
||||
}
|
||||
}
|
||||
|
||||
form.addEventListener("submit",function(e){
|
||||
var msg=validate(input.value);
|
||||
if(msg){
|
||||
e.preventDefault();
|
||||
showError(msg);
|
||||
input.focus();
|
||||
}
|
||||
});
|
||||
|
||||
input.addEventListener("input",function(){
|
||||
if(!error.hidden)showError(validate(input.value));
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
+149
-13
@@ -417,10 +417,11 @@ async fn whitespace_ip_query_falls_back_to_cf_header() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ip_not_in_database_returns_500() {
|
||||
async fn non_public_ip_returns_400() {
|
||||
let app = build_test_app();
|
||||
let (status, _) = get(&app, "/json?ip=0.0.0.0").await;
|
||||
assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
|
||||
let (status, body) = get(&app, "/json?ip=0.0.0.0").await;
|
||||
assert_eq!(status, StatusCode::BAD_REQUEST);
|
||||
assert_eq!(body, "Only public IP addresses can be queried");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -780,7 +781,7 @@ async fn browser_invalid_ip_shows_alert_html() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn browser_db_lookup_failed_shows_alert_html() {
|
||||
async fn browser_non_public_ip_shows_alert_html() {
|
||||
let app = build_test_app();
|
||||
let (status, body) = get_with_headers(
|
||||
&app,
|
||||
@@ -791,7 +792,7 @@ async fn browser_db_lookup_failed_shows_alert_html() {
|
||||
)],
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
|
||||
assert_eq!(status, StatusCode::BAD_REQUEST);
|
||||
assert!(body.contains("<!DOCTYPE html>"));
|
||||
assert!(body.contains("alert("));
|
||||
}
|
||||
@@ -875,8 +876,7 @@ async fn trailing_slash_redirects_to_canonical() {
|
||||
#[tokio::test]
|
||||
async fn trailing_slash_preserves_query_params() {
|
||||
let app = build_test_app_with_dev_mode(true);
|
||||
let response =
|
||||
get_response(&app, "/de/?ip=1.2.3.4", vec![("User-Agent", "Mozilla/5.0")]).await;
|
||||
let response = get_response(&app, "/de/?ip=1.2.3.4", vec![("User-Agent", "Mozilla/5.0")]).await;
|
||||
assert_eq!(response.status(), StatusCode::PERMANENT_REDIRECT);
|
||||
let location = response
|
||||
.headers()
|
||||
@@ -950,12 +950,7 @@ async fn error_page_has_localized_go_home_link() {
|
||||
#[tokio::test]
|
||||
async fn german_page_contains_translated_escaped_quote_strings() {
|
||||
let app = build_test_app_with_dev_mode(true);
|
||||
let (status, body) = get_with_headers(
|
||||
&app,
|
||||
"/de",
|
||||
vec![("User-Agent", "Mozilla/5.0")],
|
||||
)
|
||||
.await;
|
||||
let (status, body) = get_with_headers(&app, "/de", vec![("User-Agent", "Mozilla/5.0")]).await;
|
||||
assert_eq!(status, StatusCode::OK);
|
||||
// These strings were previously untranslated due to escaped quotes in PO files.
|
||||
assert!(
|
||||
@@ -975,3 +970,144 @@ async fn german_page_contains_translated_escaped_quote_strings() {
|
||||
"German page should not contain English data source string"
|
||||
);
|
||||
}
|
||||
|
||||
// --- Non-public IP rejection tests ---
|
||||
|
||||
#[tokio::test]
|
||||
async fn private_ipv4_rejected_on_all_endpoints() {
|
||||
let app = build_test_app();
|
||||
let private_ips = [
|
||||
"10.0.0.1",
|
||||
"172.16.0.1",
|
||||
"192.168.1.1",
|
||||
"127.0.0.1",
|
||||
"0.0.0.0",
|
||||
"255.255.255.255",
|
||||
"169.254.1.1",
|
||||
"100.64.0.1",
|
||||
];
|
||||
let endpoints = [
|
||||
"/json", "/ip", "/asn", "/org", "/country", "/city", "/proxy", "/vpn", "/hosting", "/tor",
|
||||
];
|
||||
|
||||
for ip in private_ips {
|
||||
for endpoint in &endpoints {
|
||||
let uri = format!("{endpoint}?ip={ip}");
|
||||
let (status, body) = get(&app, &uri).await;
|
||||
assert_eq!(
|
||||
status,
|
||||
StatusCode::BAD_REQUEST,
|
||||
"expected 400 for {endpoint}?ip={ip}, got {status}"
|
||||
);
|
||||
assert_eq!(
|
||||
body, "Only public IP addresses can be queried",
|
||||
"wrong error message for {endpoint}?ip={ip}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn private_ipv6_rejected() {
|
||||
let app = build_test_app();
|
||||
let private_ips = [
|
||||
"::1",
|
||||
"::",
|
||||
"fe80::1",
|
||||
"fc00::1",
|
||||
"fd00::1",
|
||||
"ff02::1",
|
||||
"2001:db8::1",
|
||||
"::ffff:127.0.0.1",
|
||||
"::ffff:192.168.1.1",
|
||||
];
|
||||
for ip in private_ips {
|
||||
let uri = format!("/json?ip={ip}");
|
||||
let (status, body) = get(&app, &uri).await;
|
||||
assert_eq!(
|
||||
status,
|
||||
StatusCode::BAD_REQUEST,
|
||||
"expected 400 for /json?ip={ip}, got {status}"
|
||||
);
|
||||
assert_eq!(
|
||||
body, "Only public IP addresses can be queried",
|
||||
"wrong error message for /json?ip={ip}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn public_ips_still_accepted() {
|
||||
let app = build_test_app();
|
||||
let (status, _) = get(&app, "/json?ip=1.1.1.1").await;
|
||||
assert_eq!(status, StatusCode::OK);
|
||||
|
||||
let (status, _) = get(&app, "/json?ip=2606:4700:4700::1111").await;
|
||||
assert_eq!(status, StatusCode::OK);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn private_ip_via_cf_header_still_works() {
|
||||
let app = build_test_app();
|
||||
let (status, body) = get_with_headers(
|
||||
&app,
|
||||
"/",
|
||||
vec![
|
||||
("User-Agent", "curl/8.7.1"),
|
||||
("CF-Connecting-IP", "10.0.0.1"),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, StatusCode::OK);
|
||||
assert_eq!(body.trim(), "10.0.0.1");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn browser_private_ipv4_shows_error() {
|
||||
let app = build_test_app();
|
||||
let (status, body) = get_with_headers(
|
||||
&app,
|
||||
"/en?ip=192.168.1.1",
|
||||
vec![(
|
||||
"User-Agent",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||
)],
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, StatusCode::BAD_REQUEST);
|
||||
assert!(body.contains("<!DOCTYPE html>"));
|
||||
assert!(body.contains("alert("));
|
||||
assert!(body.contains("192.168.1.1"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn browser_loopback_shows_error() {
|
||||
let app = build_test_app();
|
||||
let (status, body) = get_with_headers(
|
||||
&app,
|
||||
"/en?ip=127.0.0.1",
|
||||
vec![(
|
||||
"User-Agent",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||
)],
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, StatusCode::BAD_REQUEST);
|
||||
assert!(body.contains("alert("));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn browser_broadcast_shows_error() {
|
||||
let app = build_test_app();
|
||||
let (status, body) = get_with_headers(
|
||||
&app,
|
||||
"/en?ip=255.255.255.255",
|
||||
vec![(
|
||||
"User-Agent",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||
)],
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, StatusCode::BAD_REQUEST);
|
||||
assert!(body.contains("alert("));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user