troglobit/inadyn

pokbun API does not work, leaks secret key in debug logs

lunik1 opened this issue · 3 comments

I am running the latest head (7d576c4) and trying to update the DNS records of a domain whose DNS is managed by porkbun.

This config fails to update the DNS records:

provider porkbun.com {
    username = <apikey>
    secretapikey = <secretapikey>
    hostname = <my_hostname>
}

Debug log error:

HTTP 404: Unexpected status code.
Zone '<my_hostname>' not found.
Error response from DDNS server, exiting!
Error code 48: DDNS server response not OK

After looking into the code there seem to be a few issues with it:

Firstly, the example configuration seems to be wrong

provider porkbun.com {
    checkip-server = checkip.porkbun.com
    username = zone.com
    password = api_token #Create a unique custom api token with the following permissions: Zone.Zone - Read, Zone.DNS - Edit.
    hostname = yourhostname.yourdomain.com
    ttl = 400 #optional, by default is 300 seconds
}

When an API token is created with porkbun there does not seem to be a way to restrict its permissions. Also, an API token consists of both an API key and a Secret API Key: in the code the username and password are used for these respectively, but this is not reflected in the example. I have tried setting the username to my domain name, as in the example, but I get the same issue.

The code seems to use the wrong url for the porkbun API: api.porkbun.com/client/v4. The official documentation uses porkbun.com/api/json/v3/ - there is no API v4 as far as I can make out. I suspect this is the cause of the issue as this api.porkbun.com/client/v4 address does not seem to work using curl in the command line while porkbun.com/api/json/v3/ does (although I haven't been able to reproduce exactly, I get stuck in a redirect loop rather than hitting a 404).

Finally, with debug logging enabled, the code will print the secret API key when making a request. This should be censored or there should be a warning that debug logging can contain sensitive information.

Thanks for the Analysis.
Have you tried to Change the endpoint URL in porkbun.c?

Well, I have tried it now. It is not that simple:

GET /api/json/v3/dns/retrieve/x.name HTTP/1.0
Host: porkbun.com
User-Agent: inadyn/2.12.0 https://github.com/troglobit/inadyn/issues
Accept: */*
Content-Type: application/json
Content-Length: 68

inadyn[3046142]: Successfully sent HTTPS request!
inadyn[3046142]: Successfully received HTTPS response (912/8191 bytes)!
inadyn[3046142]: Response:
HTTP/1.1 400 Method Not Allowed
Date: Sat, 25 May 2024 07:18:17 GMT
Content-Type: application/json
Connection: close
Server: openresty
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload

{"status":"ERROR","message":"All HTTP request must use POST."}

Changing it to GET in porkbun.c gives me
{"status":"ERROR","message":"All API requests must provide minimal required data."}
Which is pretty odd, as the only data required is the api-keys - which are provided apparently in the json.

Here my changes up to now:
-the first two lines
-post instead of get

#define API_HOST "porkbun.com"
#define API_URL "/api/json/v3"



/* https://kb.porkbun.com/article/190-getting-started-with-the-porkbun-api */
static const char *PORKBUN_ZONE_ID_REQUEST = "POST " API_URL "/dns/retrieve/%s HTTP/1.0\r\n"    \
        "Host: " API_HOST "\r\n"                \
        "User-Agent: %s\r\n"                    \
        "Accept: */*\r\n"                               \
        "Content-Type: application/json\r\n" \
        "Content-Length: %zd\r\n\r\n" \
        "{\"apikey\":\"%s\",\"secretapikey\":\"%s\"}";

static const char *PORKBUN_HOSTNAME_ID_REQUEST_BY_NAME  = "POST " API_URL "/dns/retrieve/%s HTTP/1.0\r\n"       \
        "Host: " API_HOST "\r\n"                \
        "User-Agent: %s\r\n"                    \
        "Accept: */*\r\n"                               \
        "Content-Type: application/json\r\n" \
        "Content-Length: %zd\r\n\r\n" \
        "{\"apikey\":\"%s\",\"secretapikey\":\"%s\"}";

The complete file https://paste.debian.net/hidden/1c957794/

The error @henfri is because the Content-Length is not being calculated properly. I haven't gotten this to work, but here is a summary of what I found (including henfri's prior work):

  1. Correct API_URL and HTTP Method from GET to POST
#define CHECK(fn)       { rc = (fn); if (rc) goto cleanup; }

#define API_HOST "api.porkbun.com"
-#define API_URL "/client/v4"
+#define API_URL "/api/json/v3"

/* https://kb.porkbun.com/article/190-getting-started-with-the-porkbun-api */
-static const char *PORKBUN_ZONE_ID_REQUEST = "GET " API_URL "/dns/retrieve/%s HTTP/1.0\r\n"    \
+static const char *PORKBUN_ZONE_ID_REQUEST = "POST " API_URL "/dns/retrieve/%s HTTP/1.0\r\n"   \
       "Host: " API_HOST "\r\n"                \
       "User-Agent: %s\r\n"                    \
       "Accept: */*\r\n"                               \
@@ -36,7 +36,7 @@ static const char *PORKBUN_ZONE_ID_REQUEST = "GET " API_URL "/dns/retrieve/%s HT
       "Content-Length: %zd\r\n\r\n" \
       "{\"apikey\":\"%s\",\"secretapikey\":\"%s\"}";

-static const char *PORKBUN_HOSTNAME_ID_REQUEST_BY_NAME = "GET " API_URL "/dns/retrieve/%s HTTP/1.0\r\n"        \
+static const char *PORKBUN_HOSTNAME_ID_REQUEST_BY_NAME = "POST " API_URL "/dns/retrieve/%s HTTP/1.0\r\n"       \
       "Host: " API_HOST "\r\n"                \
       "User-Agent: %s\r\n"                    \
       "Accept: */*\r\n"                               \
  1. Correct SUCCESS token case
@@ -123,8 +123,10 @@ static int check_success(const char *json, const jsmntok_t tokens[], const int n
        for (i = 1; i < num_tokens; i++) {
                int set;

-               if (jsoneq(json, tokens + i, "success") != 0)
+               if (jsoneq(json, tokens + i, "SUCCESS") != 0) {
  1. Correct calculation of Content-Length in setup API call. Originally, POST body length was set to len(api key), but the correct length is len({"apikey":"$API_KEY","secretapikey":,"$SECRET_API_KEY"}).
@@ -294,7 +304,7 @@ static int setup(ddns_t *ctx, ddns_info_t *info, ddns_alias_t *hostname)
                       PORKBUN_ZONE_ID_REQUEST,
                       zone_name,
                       info->user_agent,
-                      strlen(info->creds.username),
+                      strlen("{\"apikey\":\"") + strlen(info->creds.username) + strlen("\",\"secretapikey\":\"") + strlen(info->creds.password) + strlen("\"}"),
                       info->creds.username,
                       info->creds.password);