Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/api/2fa.c
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
#include "config/config.h"
// getrandom()
#include "daemon.h"
// generate_app_password()
// generate_password()
#include "config/password.h"

// TOTP+HMAC
Expand Down Expand Up @@ -313,7 +313,7 @@ int generateAppPw(struct ftl_conn *api)
{
// Generate and set app password
char *password = NULL, *pwhash = NULL;
if(!generate_app_password(&password, &pwhash))
if(!generate_password(&password, &pwhash))
{
return send_json_error(api,
500,
Expand Down
18 changes: 2 additions & 16 deletions src/api/api.c
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ int api_handler(struct mg_connection *conn, void *ignored)
double_time(),
{ false, NULL, NULL, NULL, 0u },
{ false },
NULL,
{ API_FLAG_NONE, 0 }
};

Expand Down Expand Up @@ -171,22 +172,7 @@ int api_handler(struct mg_connection *conn, void *ignored)
}

// Verify requesting client is allowed to see this resource
if(api_request[i].func == api_search)
{
// Handle /api/search special as it may be allowed for local users due to webserver.api.searchAPIauth
if(!config.webserver.api.searchAPIauth.v.b && is_local_api_user(api.request->remote_addr))
{
// Local users does not need to authenticate when searchAPIauth is false
;
}
else if(api_request[i].require_auth && check_client_auth(&api, true) == API_AUTH_UNAUTHORIZED)
{
// Users need to authenticate but authentication failed
unauthorized = true;
break;
}
}
else if(api_request[i].require_auth && check_client_auth(&api, true) == API_AUTH_UNAUTHORIZED)
if(api_request[i].require_auth && check_client_auth(&api, true) == API_AUTH_UNAUTHORIZED)
{
unauthorized = true;
break;
Expand Down
28 changes: 8 additions & 20 deletions src/api/auth.c
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,6 @@ bool __attribute__((pure)) is_local_api_user(const char *remote_addr)
// Returns >= 0 for any valid authentication
int check_client_auth(struct ftl_conn *api, const bool is_api)
{
// Is the user requesting from localhost?
// This may be allowed without authentication depending on the configuration
if(!config.webserver.api.localAPIauth.v.b && is_local_api_user(api->request->remote_addr))
{
api->message = "no auth required for local user";
add_request_info(api, NULL);
return API_AUTH_LOCALHOST;
}

// When the pwhash is unset, authentication is disabled
if(config.webserver.api.pwhash.v.s[0] == '\0')
{
Expand Down Expand Up @@ -282,6 +273,7 @@ int check_client_auth(struct ftl_conn *api, const bool is_api)
}

api->user_id = user_id;
api->session = &auth_data[user_id];

api->message = "correct password";
return user_id;
Expand Down Expand Up @@ -309,6 +301,7 @@ static int get_all_sessions(struct ftl_conn *api, cJSON *json)
JSON_REF_STR_IN_OBJECT(session, "remote_addr", auth_data[i].remote_addr);
JSON_REF_STR_IN_OBJECT(session, "user_agent", auth_data[i].user_agent);
JSON_ADD_BOOL_TO_OBJECT(session, "app", auth_data[i].app);
JSON_ADD_BOOL_TO_OBJECT(session, "cli", auth_data[i].cli);
JSON_ADD_ITEM_TO_ARRAY(sessions, session);
}
JSON_ADD_ITEM_TO_OBJECT(json, "sessions", sessions);
Expand All @@ -320,7 +313,7 @@ static int get_session_object(struct ftl_conn *api, cJSON *json, const int user_
cJSON *session = JSON_NEW_OBJECT();

// Authentication not needed
if(user_id == API_AUTH_LOCALHOST || user_id == API_AUTH_EMPTYPASS)
if(user_id == API_AUTH_EMPTYPASS)
{
JSON_ADD_BOOL_TO_OBJECT(session, "valid", true);
JSON_ADD_BOOL_TO_OBJECT(session, "totp", strlen(config.webserver.api.totp_secret.v.s) > 0);
Expand Down Expand Up @@ -416,14 +409,6 @@ static int send_api_auth_status(struct ftl_conn *api, const int user_id, const t
JSON_SEND_OBJECT_CODE(json, 401); // 401 Unauthorized
}
}
else if(user_id == API_AUTH_LOCALHOST)
{
log_debug(DEBUG_API, "API Auth status: OK (localhost does not need auth)");

cJSON *json = JSON_NEW_OBJECT();
get_session_object(api, json, user_id, now);
JSON_SEND_OBJECT(json);
}
else if(user_id == API_AUTH_EMPTYPASS)
{
log_debug(DEBUG_API, "API Auth status: OK (empty password)");
Expand Down Expand Up @@ -536,7 +521,9 @@ int api_auth(struct ftl_conn *api)
else
result = verify_login(password);

if(result == PASSWORD_CORRECT || result == APPPASSWORD_CORRECT)
if(result == PASSWORD_CORRECT ||
result == APPPASSWORD_CORRECT ||
result ==CLIPASSWORD_CORRECT)
{
// Accepted

Expand All @@ -547,7 +534,7 @@ int api_auth(struct ftl_conn *api)

// Check possible 2FA token
// Successful login with empty password does not require 2FA
if(strlen(config.webserver.api.totp_secret.v.s) > 0 && result != APPPASSWORD_CORRECT)
if(strlen(config.webserver.api.totp_secret.v.s) > 0 && result == PASSWORD_CORRECT)
{
// Get 2FA token from payload
cJSON *json_totp;
Expand Down Expand Up @@ -618,6 +605,7 @@ int api_auth(struct ftl_conn *api)
auth_data[i].tls.login = api->request->is_ssl;
auth_data[i].tls.mixed = false;
auth_data[i].app = result == APPPASSWORD_CORRECT;
auth_data[i].cli = result == CLIPASSWORD_CORRECT;

// Generate new SID and CSRF token
generateSID(auth_data[i].sid);
Expand Down
1 change: 1 addition & 0 deletions src/api/auth.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
struct session {
bool used;
bool app;
bool cli;
struct {
bool login;
bool mixed;
Expand Down
19 changes: 19 additions & 0 deletions src/api/config.c
Original file line number Diff line number Diff line change
Expand Up @@ -1058,6 +1058,25 @@ int api_config(struct ftl_conn *api)
if(api->method == HTTP_GET)
return api_config_get(api);

// Check if this is an app session and reject the request if app sudo
// mode is disabled
if(api->session != NULL && api->session->app && !config.webserver.api.app_sudo.v.b)
{
return send_json_error(api, 403,
"forbidden",
"Unable to change configuration (read-only)",
"The current app session is not allowed to modify Pi-hole config settings (webserver.api.app_sudo is false)");
}

// Check if this is a CLI session and reject the request
if(api->session != NULL && api->session->cli)
{
return send_json_error(api, 403,
"forbidden",
"Unable to change configuration (read-only)",
"The current CLI session is not allowed to modify Pi-hole config settings");
}

// POST: Create a new config (not supported)
// PATCH: Replace parts of the the config with the provided one
// PUT: Replaces the entire config with the provided one (not supported
Expand Down
4 changes: 4 additions & 0 deletions src/api/docs/content/specs/auth.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,9 @@ components:
app:
type: boolean
description: Indicator if this session was initiated using an application password
cli:
type: boolean
description: Indicator if this session was initiated using the command-line interface (CLI)
login_at:
type: integer
description: Timestamp of login (seconds since epoch)
Expand All @@ -368,6 +371,7 @@ components:
login: true
mixed: false
app: false
cli: false
login_at: 1580000000
last_active: 1580000000
valid_until: 1580000300
Expand Down
12 changes: 6 additions & 6 deletions src/api/docs/content/specs/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -432,10 +432,6 @@ components:
api:
type: object
properties:
localAPIauth:
type: boolean
searchAPIauth:
type: boolean
max_sessions:
type: integer
prettyJSON:
Expand All @@ -450,6 +446,10 @@ components:
type: string
app_pwhash:
type: string
app_sudo:
type: boolean
cli_pw:
type: boolean
excludeClients:
type: array
items:
Expand Down Expand Up @@ -745,14 +745,14 @@ components:
boxed: true
theme: "default-darker"
api:
localAPIauth: false
searchAPIauth: false
max_sessions: 16
prettyJSON: false
password: "********"
pwhash: ''
totp_secret: ''
app_pwhash: ''
app_sudo: false
cli_pw: true
excludeClients: [ '1\.2\.3\.4', 'localhost', 'fe80::345' ]
excludeDomains: [ 'google\\.de', 'pi-hole\.net' ]
maxHistory: 86400
Expand Down
1 change: 0 additions & 1 deletion src/api/docs/content/specs/search.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ components:
The optional parameters `N` and `partial` limit the maximum number of returned records and whether partial matches should be returned, respectively.
There is a hard upper limit of `N` defined in FTL (currently set to 10,000) to ensure that the response is not too large.
ABP matches are not returned when partial matching is requested.
Depending on the value of the config option webserver.api.searchAPIauth, local clients may not need to authenticate for this endpoint.
International domains names (IDNs) are internally converted to punycode before matching.
responses:
'200':
Expand Down
24 changes: 12 additions & 12 deletions src/config/config.c
Original file line number Diff line number Diff line change
Expand Up @@ -1031,18 +1031,6 @@ void initConfig(struct config *conf)
conf->webserver.interface.theme.c = validate_stub; // Only type-based checking

// sub-struct api
conf->webserver.api.searchAPIauth.k = "webserver.api.searchAPIauth";
conf->webserver.api.searchAPIauth.h = "Do local clients need to authenticate to access the search API? This settings allows local clients to use pihole -q ... without authentication. Note that \"local\" in the sense of the option means only 127.0.0.1 and [::1]";
conf->webserver.api.searchAPIauth.t = CONF_BOOL;
conf->webserver.api.searchAPIauth.d.b = false;
conf->webserver.api.searchAPIauth.c = validate_stub; // Only type-based checking

conf->webserver.api.localAPIauth.k = "webserver.api.localAPIauth";
conf->webserver.api.localAPIauth.h = "Do local clients need to authenticate to access the API? This settings allows local clients to use the API without authentication.";
conf->webserver.api.localAPIauth.t = CONF_BOOL;
conf->webserver.api.localAPIauth.d.b = true;
conf->webserver.api.localAPIauth.c = validate_stub; // Only type-based checking

conf->webserver.api.max_sessions.k = "webserver.api.max_sessions";
conf->webserver.api.max_sessions.h = "Number of concurrent sessions allowed for the API. If the number of sessions exceeds this value, no new sessions will be allowed until the number of sessions drops due to session expiration or logout. Note that the number of concurrent sessions is irrelevant if authentication is disabled as no sessions are used in this case.";
conf->webserver.api.max_sessions.t = CONF_UINT16;
Expand Down Expand Up @@ -1088,6 +1076,18 @@ void initConfig(struct config *conf)
conf->webserver.api.app_pwhash.d.s = (char*)"";
conf->webserver.api.app_pwhash.c = validate_stub; // Only type-based checking

conf->webserver.api.app_sudo.k = "webserver.api.app_sudo";
conf->webserver.api.app_sudo.h = "Should application password API sessions be allowed to modify config settings?\n Setting this to true allows third-party applications using the application password to modify advanced settings, e.g., the upstream DNS servers, DHCP server settings, or changing passwords. This setting should only be enabled if really needed and only if you trust the applications using the application password.";
conf->webserver.api.app_sudo.t = CONF_BOOL;
conf->webserver.api.app_sudo.d.b = false;
conf->webserver.api.app_sudo.c = validate_stub; // Only type-based checking

conf->webserver.api.cli_pw.k = "webserver.api.cli_pw";
conf->webserver.api.cli_pw.h = "Should FTL create a temporary CLI password? This password is stored in clear in /etc/pihole and can be used by the CLI (pihole ... commands) to authenticate against the API. Note that the password is only valid for the current session and regenerated on each FTL restart. Sessions initiated with this password cannot modify the Pi-hole configuration (change passwords, etc.) for security reasons but can still use the API to query data and manage lists.";
conf->webserver.api.cli_pw.t = CONF_BOOL;
conf->webserver.api.cli_pw.d.b = true;
conf->webserver.api.cli_pw.c = validate_stub; // Only type-based checking

conf->webserver.api.excludeClients.k = "webserver.api.excludeClients";
conf->webserver.api.excludeClients.h = "Array of clients to be excluded from certain API responses (regex):\n - Query Log (/api/queries)\n - Top Clients (/api/stats/top_clients)\n This setting accepts both IP addresses (IPv4 and IPv6) as well as hostnames.\n Note that backslashes \"\\\" need to be escaped, i.e. \"\\\\\" in this setting\n\n Example: [ \"^192\\\\.168\\\\.2\\\\.56$\", \"^fe80::341:[0-9a-f]*$\", \"^localhost$\" ]";
conf->webserver.api.excludeClients.a = cJSON_CreateStringReference("array of regular expressions describing clients");
Expand Down
4 changes: 2 additions & 2 deletions src/config/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -251,14 +251,14 @@ struct config {
struct conf_item theme;
} interface;
struct {
struct conf_item localAPIauth;
struct conf_item searchAPIauth;
struct conf_item max_sessions;
struct conf_item prettyJSON;
struct conf_item pwhash;
struct conf_item password; // This is a pseudo-item
struct conf_item totp_secret; // This is a write-only item
struct conf_item app_pwhash;
struct conf_item app_sudo;
struct conf_item cli_pw;
struct conf_item excludeClients;
struct conf_item excludeDomains;
struct conf_item maxHistory;
Expand Down
5 changes: 0 additions & 5 deletions src/config/legacy_reader.c
Original file line number Diff line number Diff line change
Expand Up @@ -309,11 +309,6 @@ const char *readFTLlegacy(struct config *conf)
if(buffer != NULL)
conf->webserver.acl.v.s = strdup(buffer);

// API_AUTH_FOR_LOCALHOST
// defaults to: true
buffer = parseFTLconf(fp, "API_AUTH_FOR_LOCALHOST");
parseBool(buffer, &conf->webserver.api.localAPIauth.v.b);

// API_SESSION_TIMEOUT
// How long should a session be considered valid after login?
// defaults to: 300 seconds
Expand Down
Loading