[Security] Add IP-based filtering to web interface and http-commands. (#653)

* [mega] Detect client IP for SysInfo page and some cleanup

Added formatIP functions for readability and add client IP to SystemInfo page as preparation to detect origin just to get an idea of possibilities for #647.
Also added some reserve() statements to Strings to prevent heap fragmentation.

* [mega] Cleanup formatIP calls

* [security] #647 Add IP filtering to web interface and commands

As discussed in #647:
In the Config tab, a "Client IP filtering" section is added.
This allows for 3 levels of IP filtering:
* "Allow All" - does not imply any filtering based on IP address of the client.
* "Allow Local Subnet" - Allow only clients from within the local subnet, based on IP and subnetmask of the ESP.
* "Allow IP range" - Allow only clients within a range of IP-addresses.

Settings like these are very likely to lock out the rightful owner, so there are also two commands added, which can be sent via the serial connection.
* accessinfo
* clearaccessblock

The first one just displays the current active IP filtering.
The "clearaccessblock" command will temporarily disable the access filtering. This allows the user to login and reset the access filtering.

"Allow Local Subnet" is set as factory default.
Only when first loading a new firmware with this feature and existing configuration, the "All Allowed" is active (the old default)
This commit is contained in:
Gijs Noorlander
2017-12-31 15:31:20 +01:00
committed by DatuX
parent a9a741f775
commit fae1edc859
6 changed files with 262 additions and 54 deletions
+15
View File
@@ -179,6 +179,21 @@ void ExecuteCommand(byte source, const char *Line)
delay(60000);
}
if (strcasecmp_P(Command, PSTR("accessinfo")) == 0)
{
success = true;
Serial.print(F("Allowed IP range : "));
Serial.println(describeAllowedIPrange());
}
if (strcasecmp_P(Command, PSTR("clearaccessblock")) == 0)
{
success = true;
clearAccessBlock();
Serial.print(F("Allowed IP range : "));
Serial.println(describeAllowedIPrange());
}
if (strcasecmp_P(Command, PSTR("meminfo")) == 0)
{
success = true;
+8 -2
View File
@@ -90,6 +90,9 @@
#define DEFAULT_DNS "192.168.0.1" // Enter your DNS
#define DEFAULT_GW "192.168.0.1" // Enter your gateway
#define DEFAULT_SUBNET "255.255.255.0" // Enter your subnet
#define DEFAULT_IPRANGE_LOW "0.0.0.0" // Allowed IP range to access webserver
#define DEFAULT_IPRANGE_HIGH "255.255.255.255" // Allowed IP range to access webserver
#define DEFAULT_IP_BLOCK_LEVEL 1 // 0: ALL_ALLOWED 1: LOCAL_SUBNET_ALLOWED 2: ONLY_IP_RANGE_ALLOWED
#define DEFAULT_CONTROLLER false // true or false enabled or disabled, set 1st controller defaults
// using a default template, you also need to set a DEFAULT PROTOCOL to a suitable MQTT protocol !
@@ -357,7 +360,7 @@
#include <WiFi.h>
#include <ESP32WebServer.h>
#include "SPIFFS.h"
ESP32WebServer WebServer(80);
ESP32WebServer WebServer(80);
#ifdef FEATURE_MDNS
#include <ESPmDNS.h>
#endif
@@ -412,6 +415,9 @@ struct SecurityStruct
char ControllerUser[CONTROLLER_MAX][26];
char ControllerPassword[CONTROLLER_MAX][64];
char Password[26];
byte AllowedIPrangeLow[4]; // TD-er: Use these
byte AllowedIPrangeHigh[4];
byte IPblockLevel;
//its safe to extend this struct, up to 4096 bytes, default values in config are 0
} SecuritySettings;
@@ -813,7 +819,7 @@ void setup()
fileSystemCheck();
LoadSettings();
if (strcasecmp(SecuritySettings.WifiSSID, "ssid") == 0)
wifiSetup = true;
+25 -7
View File
@@ -1092,6 +1092,11 @@ void ResetFactory(void)
strcpy_P(SecuritySettings.WifiKey, PSTR(DEFAULT_KEY));
strcpy_P(SecuritySettings.WifiAPKey, PSTR(DEFAULT_AP_KEY));
SecuritySettings.Password[0] = 0;
// TD-er Reset access control
str2ip((char*)DEFAULT_IPRANGE_LOW, SecuritySettings.AllowedIPrangeLow);
str2ip((char*)DEFAULT_IPRANGE_HIGH, SecuritySettings.AllowedIPrangeHigh);
SecuritySettings.IPblockLevel = DEFAULT_IP_BLOCK_LEVEL;
Settings.Delay = DEFAULT_DELAY;
Settings.Pin_i2c_sda = 4;
Settings.Pin_i2c_scl = 5;
@@ -1668,6 +1673,23 @@ boolean matchClockEvent(unsigned long clockEvent, unsigned long clockSet)
Parse string template
\*********************************************************************************************/
// Call this by first declaring a char array of size 20, like:
// char strIP[20];
// formatIP(ip, strIP);
void formatIP(const IPAddress& ip, char (&strIP)[20]) {
sprintf_P(strIP, PSTR("%u.%u.%u.%u"), ip[0], ip[1], ip[2], ip[3]);
}
String formatIP(const IPAddress& ip) {
char strIP[20];
formatIP(ip, strIP);
return String(strIP);
}
void formatMAC(const uint8_t* mac, char (&strMAC)[20]) {
sprintf_P(strMAC, PSTR("%02X:%02X:%02X:%02X:%02X:%02X"), mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
}
String parseTemplate(String &tmpString, byte lineSize)
{
String newString = "";
@@ -1774,10 +1796,8 @@ String parseTemplate(String &tmpString, byte lineSize)
newString.replace(F("%vcc%"), String(vcc));
#endif
IPAddress ip = WiFi.localIP();
char strIP[20];
sprintf_P(strIP, PSTR("%u.%u.%u.%u"), ip[0], ip[1], ip[2], ip[3]);
newString.replace(F("%ip%"), strIP);
const IPAddress ip = WiFi.localIP();
newString.replace(F("%ip%"), formatIP(ip));
newString.replace(F("%ip1%"), String(ip[0]));
newString.replace(F("%ip2%"), String(ip[1]));
newString.replace(F("%ip3%"), String(ip[2]));
@@ -2262,10 +2282,8 @@ unsigned long getNtpTime()
else
WiFi.hostByName(ntpServerName, timeServerIP);
char host[20];
sprintf_P(host, PSTR("%u.%u.%u.%u"), timeServerIP[0], timeServerIP[1], timeServerIP[2], timeServerIP[3]);
log = F("NTP : NTP send to ");
log += host;
log += formatIP(timeServerIP);
addLog(LOG_LEVEL_DEBUG_MORE, log);
while (udp.parsePacket() > 0) ; // discard any previously received packets
+7 -9
View File
@@ -65,7 +65,7 @@ void checkUDP()
return;
runningUPDCheck = true;
// UDP events
int packetSize = portUDP.parsePacket();
if (packetSize)
@@ -138,9 +138,9 @@ void checkUDP()
}
char macaddress[20];
sprintf_P(macaddress, PSTR("%02x:%02x:%02x:%02x:%02x:%02x"), mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
char ipaddress[16];
sprintf_P(ipaddress, PSTR("%u.%u.%u.%u"), ip[0], ip[1], ip[2], ip[3]);
formatMAC(mac, macaddress);
char ipaddress[20];
formatIP(ip, ipaddress);
char log[80];
sprintf_P(log, PSTR("UDP : %s,%s,%u"), macaddress, ipaddress, unit);
addLog(LOG_LEVEL_DEBUG_MORE, log);
@@ -429,10 +429,8 @@ void SSDP_schema(WiFiClient &client) {
return;
}
IPAddress ip = WiFi.localIP();
char str[20];
sprintf_P(str, PSTR("%u.%u.%u.%u"), ip[0], ip[1], ip[2], ip[3]);
uint32_t chipId = ESP.getChipId();
const IPAddress ip = WiFi.localIP();
const uint32_t chipId = ESP.getChipId();
char uuid[64];
sprintf_P(uuid, PSTR("38323636-4558-4dda-9188-cda0e6%02x%02x%02x"),
(uint16_t) ((chipId >> 16) & 0xff),
@@ -452,7 +450,7 @@ void SSDP_schema(WiFiClient &client) {
"<minor>0</minor>"
"</specVersion>"
"<URLBase>http://");
ssdp_schema += str;
ssdp_schema += formatIP(ip);
ssdp_schema += F(":80/</URLBase>"
"<device>"
"<deviceType>urn:schemas-upnp-org:device:BinaryLight:1</deviceType>"
+175 -25
View File
@@ -1,3 +1,86 @@
//********************************************************************************
// Allowed IP range check
//********************************************************************************
#define ALL_ALLOWED 0
#define LOCAL_SUBNET_ALLOWED 1
#define ONLY_IP_RANGE_ALLOWED 2
boolean ipLessEqual(const IPAddress& ip, const IPAddress& high)
{
for (byte i = 0; i < 4; ++i) {
if (ip[i] > high[i]) return false;
}
return true;
}
boolean ipInRange(const IPAddress& ip, const IPAddress& low, const IPAddress& high)
{
return (ipLessEqual(low, ip) && ipLessEqual(ip, high));
}
String describeAllowedIPrange() {
String reply;
switch (SecuritySettings.IPblockLevel) {
case ALL_ALLOWED:
reply += F("All Allowed");
break;
default:
{
IPAddress low, high;
getIPallowedRange(low, high);
reply += formatIP(low);
reply += F(" - ");
reply += formatIP(high);
}
}
return reply;
}
bool getIPallowedRange(IPAddress& low, IPAddress& high)
{
switch (SecuritySettings.IPblockLevel) {
case LOCAL_SUBNET_ALLOWED:
return getSubnetRange(low, high);
case ONLY_IP_RANGE_ALLOWED:
low = SecuritySettings.AllowedIPrangeLow;
high = SecuritySettings.AllowedIPrangeHigh;
break;
default:
low = IPAddress(0,0,0,0);
high = IPAddress(255,255,255,255);
return false;
}
return true;
}
boolean clientIPallowed()
{
// TD-er Must implement "safe time after boot"
IPAddress low, high;
if (!getIPallowedRange(low, high))
{
// No subnet range determined, cannot filter on IP
return true;
}
WiFiClient client(WebServer.client());
if (ipInRange(client.remoteIP(), low, high))
return true;
String response = F("IP blocked: ");
response += formatIP(client.remoteIP());
WebServer.send(403, "text/html", response);
response += F(" Allowed: ");
response += formatIP(low);
response += F(" - ");
response += formatIP(high);
addLog(LOG_LEVEL_ERROR, response);
return false;
}
void clearAccessBlock()
{
SecuritySettings.IPblockLevel = ALL_ALLOWED;
}
//********************************************************************************
// Web Interface init
//********************************************************************************
@@ -108,14 +191,13 @@ void WebServerInit()
void sendWebPage(const String& tmplName, String& pageContent)
{
String pageTemplate;
String fileName = tmplName;
fileName += F(".htm");
fs::File f = SPIFFS.open(fileName, "r+");
String pageTemplate;
if (f)
{
pageTemplate.reserve(f.size());
while (f.available())
pageTemplate += (char)f.read();
f.close();
@@ -465,8 +547,9 @@ void handle_root() {
printToWeb = true;
printWebString = "";
if (sCommand.length() > 0)
if (sCommand.length() > 0) {
ExecuteCommand(VALUE_SOURCE_HTTP, sCommand.c_str());
}
IPAddress ip = WiFi.localIP();
// IPAddress gw = WiFi.gatewayIP();
@@ -516,10 +599,8 @@ void handle_root() {
reply += lowestRAMfunction;
reply += F(")");
char str[20];
sprintf_P(str, PSTR("%u.%u.%u.%u"), ip[0], ip[1], ip[2], ip[3]);
reply += F("<TR><TD>IP:<TD>");
reply += str;
reply += formatIP(ip);
reply += F("<TD><TD>Wifi RSSI:<TD>");
if (WiFi.status() == WL_CONNECTED)
@@ -631,6 +712,9 @@ void handle_config() {
//String key = WebServer.arg(F("key"));
String ssid2 = WebServer.arg(F("ssid2"));
//String key2 = WebServer.arg(F("key2"));
String iprangelow = WebServer.arg(F("iprangelow"));
String iprangehigh = WebServer.arg(F("iprangehigh"));
String sensordelay = WebServer.arg(F("delay"));
String deepsleep = WebServer.arg(F("deepsleep"));
String deepsleeponfail = WebServer.arg(F("deepsleeponfail"));
@@ -658,6 +742,28 @@ void handle_config() {
//strncpy(SecuritySettings.WifiAPKey, apkey.c_str(), sizeof(SecuritySettings.WifiAPKey));
copyFormPassword(F("apkey"), SecuritySettings.WifiAPKey, sizeof(SecuritySettings.WifiAPKey));
// TD-er Read access control from form.
SecuritySettings.IPblockLevel = getFormItemInt(F("ipblocklevel"));
switch (SecuritySettings.IPblockLevel) {
case LOCAL_SUBNET_ALLOWED:
{
IPAddress low, high;
getSubnetRange(low, high);
for (byte i=0; i < 4; ++i) {
SecuritySettings.AllowedIPrangeLow[i] = low[i];
SecuritySettings.AllowedIPrangeHigh[i] = high[i];
}
break;
}
case ONLY_IP_RANGE_ALLOWED:
case ALL_ALLOWED:
iprangelow.toCharArray(tmpString, 26);
str2ip(tmpString, SecuritySettings.AllowedIPrangeLow);
iprangehigh.toCharArray(tmpString, 26);
str2ip(tmpString, SecuritySettings.AllowedIPrangeHigh);
break;
}
Settings.Delay = sensordelay.toInt();
Settings.deepSleep = (deepsleep == "on");
Settings.deepSleepOnFail = (deepsleeponfail == "on");
@@ -692,6 +798,21 @@ void handle_config() {
addFormSeparator(reply);
addFormPasswordBox(reply, F("WPA AP Mode Key"), F("apkey"), SecuritySettings.WifiAPKey, 63);
// TD-er add IP access box F("ipblocklevel")
addFormSubHeader(reply, F("Client IP filtering"));
{
IPAddress low, high;
getIPallowedRange(low, high);
byte iplow[4];
byte iphigh[4];
for (byte i = 0; i < 4; ++i) {
iplow[i] = low[i];
iphigh[i] = high[i];
}
addFormIPaccessControlSelect(reply, F("Client IP block level"), F("ipblocklevel"), SecuritySettings.IPblockLevel);
addFormIPBox(reply, F("Access IP lower range"), F("iprangelow"), iplow);
addFormIPBox(reply, F("Access IP upper range"), F("iprangehigh"), iphigh);
}
addFormSubHeader(reply, F("IP Settings"));
@@ -1225,6 +1346,21 @@ void addPinStateSelect(String& str, String name, int choice)
addSelector(str, name, 4, options, NULL, NULL, choice, false);
}
//********************************************************************************
// Add a IP Access Control select dropdown list
//********************************************************************************
void addFormIPaccessControlSelect(String& str, const String& label, const String& id, int choice)
{
addRowLabel(str, label);
addIPaccessControlSelect(str, id, choice);
}
void addIPaccessControlSelect(String& str, String name, int choice)
{
String options[3] = { F("Allow All"), F("Allow Local Subnet"), F("Allow IP range") };
addSelector(str, name, 3, options, NULL, NULL, choice, false);
}
//********************************************************************************
// Web Interface device page
@@ -2300,8 +2436,9 @@ void addFormIPBox(String& str, const String& label, const String& id, const byte
char strip[20];
if (ip[0] == 0 && ip[1] == 0 && ip[2] == 0 && ip[3] == 0)
strip[0] = 0;
else
sprintf_P(strip, PSTR("%u.%u.%u.%u"), ip[0], ip[1], ip[2], ip[3]);
else {
formatIP(ip, strip);
}
addRowLabel(str, label);
str += F("<input type='text' name='");
@@ -2846,6 +2983,7 @@ void handle_wifiscanner() {
// Web Interface login page
//********************************************************************************
void handle_login() {
if (!clientIPallowed()) return;
String webrequest = WebServer.arg(F("password"));
char command[80];
@@ -2887,6 +3025,7 @@ void handle_login() {
// Web Interface control page (no password!)
//********************************************************************************
void handle_control() {
if (!clientIPallowed()) return;
String webrequest = WebServer.arg(F("cmd"));
@@ -2931,6 +3070,7 @@ void handle_control() {
void handle_json()
{
// ToDo TD-er: Must check for allowed client IP??????
String tasknr = WebServer.arg(F("tasknr"));
String reply = "";
@@ -3152,6 +3292,7 @@ void handle_advanced() {
//********************************************************************************
boolean isLoggedIn()
{
if (!clientIPallowed()) return false;
if (SecuritySettings.Password[0] == 0)
WebLoggedIn = true;
@@ -3392,6 +3533,7 @@ bool loadFromFS(boolean spiffs, String path) {
// Web Interface custom page handler
//********************************************************************************
boolean handle_custom(String path) {
if (!clientIPallowed()) return false;
path = path.substring(1);
String reply = "";
@@ -3458,7 +3600,7 @@ boolean handle_custom(String path) {
// handle commands from a custom page
String webrequest = WebServer.arg(F("cmd"));
if (webrequest.length() > 0){
if (webrequest.length() > 0 ){
struct EventStruct TempEvent;
parseCommandString(&TempEvent, webrequest);
TempEvent.Source = VALUE_SOURCE_HTTP;
@@ -3477,6 +3619,7 @@ boolean handle_custom(String path) {
if (dataFile)
{
String page = "";
page.reserve(dataFile.size());
while (dataFile.available())
page += ((char)dataFile.read());
@@ -3525,6 +3668,7 @@ boolean handle_custom(String path) {
// Web Interface file list
//********************************************************************************
void handle_filelist() {
if (!clientIPallowed()) return;
#if defined(ESP8266)
navMenuIndex = 7;
String fdelete = WebServer.arg(F("delete"));
@@ -3614,6 +3758,7 @@ void handle_filelist() {
//********************************************************************************
#ifdef FEATURE_SD
void handle_SDfilelist() {
if (!clientIPallowed()) return;
navMenuIndex = 7;
String fdelete = "";
@@ -3783,16 +3928,16 @@ void handleNotFound() {
// Web Interface Setup Wizard
//********************************************************************************
void handle_setup() {
// Do not check client IP range allowed.
String reply = "";
addHeader(false, reply);
if (WiFi.status() == WL_CONNECTED)
{
addHtmlError(reply, SaveSettings());
IPAddress ip = WiFi.localIP();
const IPAddress ip = WiFi.localIP();
char host[20];
sprintf_P(host, PSTR("%u.%u.%u.%u"), ip[0], ip[1], ip[2], ip[3]);
formatIP(ip, host);
reply += F("<BR>ESP is connected and using IP Address: <BR><h1>");
reply += host;
reply += F("</h1><BR><BR>Connect your laptop / tablet / phone<BR>back to your main Wifi network and<BR><BR>");
@@ -4042,11 +4187,8 @@ void handle_sysinfo() {
int freeMem = ESP.getFreeHeap();
String reply = "";
reply.reserve(4096);
addHeader(true, reply);
IPAddress ip = WiFi.localIP();
IPAddress gw = WiFi.gatewayIP();
reply += printWebString;
reply += F("<form>");
reply += F("<table><TR><TH width=120>System Info<TH>");
@@ -4161,25 +4303,33 @@ void handle_sysinfo() {
reply += F(" dB)");
}
char str[20];
sprintf_P(str, PSTR("%u.%u.%u.%u"), ip[0], ip[1], ip[2], ip[3]);
reply += F("<TR><TD>IP<TD>");
reply += str;
reply += F("<TR><TD>IP / subnet<TD>");
reply += formatIP(WiFi.localIP());
reply += F(" / ");
reply += formatIP(WiFi.subnetMask());
sprintf_P(str, PSTR("%u.%u.%u.%u"), gw[0], gw[1], gw[2], gw[3]);
reply += F("<TR><TD>GW<TD>");
reply += str;
reply += formatIP(WiFi.gatewayIP());
{
reply += F("<TR><TD>Client IP<TD>");
WiFiClient client(WebServer.client());
reply += formatIP(client.remoteIP());
}
reply += F("<TR><TD>Allowed IP Range<TD>");
reply += describeAllowedIPrange();
reply += F("<TR><TD>STA MAC<TD>");
uint8_t mac[] = {0, 0, 0, 0, 0, 0};
uint8_t* macread = WiFi.macAddress(mac);
char macaddress[20];
sprintf_P(macaddress, PSTR("%02x:%02x:%02x:%02x:%02x:%02x"), macread[0], macread[1], macread[2], macread[3], macread[4], macread[5]);
formatMAC(macread, macaddress);
reply += macaddress;
reply += F("<TR><TD>AP MAC<TD>");
macread = WiFi.softAPmacAddress(mac);
sprintf_P(macaddress, PSTR("%02x:%02x:%02x:%02x:%02x:%02x"), macread[0], macread[1], macread[2], macread[3], macread[4], macread[5]);
formatMAC(macread, macaddress);
reply += macaddress;
reply += F("<TR><TD colspan=2><H3>Firmware</H3></TD></TR>");
+32 -11
View File
@@ -97,15 +97,13 @@ boolean WifiConnect(byte connectAttempts)
//use static ip?
if (Settings.IP[0] != 0 && Settings.IP[0] != 255)
{
char str[20];
sprintf_P(str, PSTR("%u.%u.%u.%u"), Settings.IP[0], Settings.IP[1], Settings.IP[2], Settings.IP[3]);
const IPAddress ip = Settings.IP;
log = F("IP : Static IP :");
log += str;
log += ip;
addLog(LOG_LEVEL_INFO, log);
IPAddress ip = Settings.IP;
IPAddress gw = Settings.Gateway;
IPAddress subnet = Settings.Subnet;
IPAddress dns = Settings.DNS;
const IPAddress gw = Settings.Gateway;
const IPAddress subnet = Settings.Subnet;
const IPAddress dns = Settings.DNS;
WiFi.config(ip, gw, subnet, dns);
}
@@ -199,10 +197,7 @@ boolean WifiConnectSSID(char WifiSSID[], char WifiKey[], byte connectAttempts)
initTime();
}
log = F("WIFI : Connected! IP: ");
IPAddress ip = WiFi.localIP();
char str[20];
sprintf_P(str, PSTR("%u.%u.%u.%u"), ip[0], ip[1], ip[2], ip[3]);
log += str;
log += formatIP(WiFi.localIP());
log += F(" (");
log += WifiGetHostname();
log += F(")");
@@ -307,3 +302,29 @@ void WifiCheck()
}
}
}
//********************************************************************************
// Return subnet range of WiFi.
//********************************************************************************
bool getSubnetRange(IPAddress& low, IPAddress& high)
{
if (WifiIsAP()) {
// WiFi is active as accesspoint, do not check.
return false;
}
if (WiFi.status() != WL_CONNECTED) {
return false;
}
const IPAddress ip = WiFi.localIP();
const IPAddress subnet = WiFi.subnetMask();
low = ip;
high = ip;
// Compute subnet range.
for (byte i=0; i < 4; ++i) {
if (subnet[i] != 255) {
low[i] = low[i] & subnet[i];
high[i] = high[i] | ~subnet[i];
}
}
return true;
}