Repository: sopel-irc/sopel

Issue:
Sopel's web lookups should not work with internal addresses like http://192.168.1.1
Example (also reproduced in freenode.net's official sopel bot via /msg command):
[01:51pm] <Ant> http://192.168.1.1
01:51PM <URL> [ 401 Unauthorized ] - 192.168.1.1
[01:51pm] <Ant> http://192.168.100.1
01:51PM <URL> [ Touchstone Status ] - 192.168.100.1


Discussion:
Indeed, this is a potential security concern for Sopel instances running on machines with LAN access to other, possibly internal-only, services.

Resolving patch:
diff --git a/sopel/modules/url.py b/sopel/modules/url.py
--- a/sopel/modules/url.py
+++ b/sopel/modules/url.py
@@ -13,11 +13,19 @@
 
 import re
 
+import dns.resolver
+import ipaddress
 import requests
 
 from sopel import __version__, module, tools, web
 from sopel.config.types import ListAttribute, StaticSection, ValidatedAttribute
 
+# Python3 vs Python2
+try:
+    from urllib.parse import urlparse
+except ImportError:
+    from urlparse import urlparse
+
 USER_AGENT = 'Sopel/{} (https://sopel.chat)'.format(__version__)
 default_headers = {'User-Agent': USER_AGENT}
 # These are used to clean up the title tag before actually parsing it. Not the
@@ -42,6 +50,10 @@ class UrlSection(StaticSection):
     shorten_url_length = ValidatedAttribute(
         'shorten_url_length', int, default=0)
     """If greater than 0, the title fetcher will include a TinyURL version of links longer than this many characters."""
+    enable_private_resolution = ValidatedAttribute(
+        'enable_private_resolution', bool, default=False)
+    enable_dns_resolution = ValidatedAttribute(
+        'enable_dns_resolution', bool, default=False)
 
 
 def configure(config):
@@ -67,6 +79,14 @@ def configure(config):
         ' shorter version of the URL in the title as a TinyURL link'
         ' (0 to disable)'
     )
+    config.url.configure_setting(
+        'enable_private_resolution',
+        'Enable URL lookups for RFC1918 addresses?'
+    )
+    config.url.configure_setting(
+        'enable_dns_resolution',
+        'Enable DNS resolution for all domains to validate if there are RFC1918 resolutions?'
+    )
 
 
 def setup(bot):
@@ -173,6 +193,26 @@ def process_urls(bot, trigger, urls):
         if check_callbacks(bot, url):
             continue
 
+        # Prevent private addresses form being queried if enable_private_resolution is False
+        if not bot.config.url.enable_private_resolution:
+            parsed = urlparse(url)
+            # Check if it's an address like http://192.168.1.1
+            try:
+                if ipaddress.ip_address(parsed.hostname).is_private or ipaddress.ip_address(parsed.hostname).is_loopback:
+                    continue
+            except ValueError:
+                pass
+
+            # Check if domains are RFC1918 addresses if enable_dns_resolutions is set
+            if bot.config.url.enable_dns_resolution:
+                private = False
+                for result in dns.resolver.query(parsed.hostname):
+                    if ipaddress.ip_address(result).is_private:
+                        private = True
+                        break
+                if private:
+                    continue
+
         # Call the URL to get a title, if possible
         title = find_title(url, verify=bot.config.core.verify_ssl)
         if not title:


Test patch: