Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Passwords leaking into server logs through API #4556

Open
skt-jdrzik opened this issue Jan 10, 2025 · 10 comments
Open

Passwords leaking into server logs through API #4556

skt-jdrzik opened this issue Jan 10, 2025 · 10 comments

Comments

@skt-jdrzik
Copy link

Steps to reproduce

Hello, I want to report two problems with current teampass API...

First problem:
I can login to API with token succesfully, but each time I try add item it always fails.
I am using this python code:

import json

def teampass_api_key(username, password, apikey):
    url = "https://tp.xx.xxx.xx/api/index.php/authorize"
    payload = { 'apikey': apikey,
                'login': username,
                'password': password}
    print(payload)
    r = httpx.post(url, params=payload)
    print(r.url)
    response = json.loads(r.text)
    return response["token"]

def create_item(token,label,folder_id,password,description,login,email,url,icon='fa-solid fa-start',anyone_can_modify=True):
    url = "https://tp.xx.xxx.xx/api/index.php/item/create"
    payload = {
        'label' : label,
        'folder_id': folder_id,
        'password': password,
        'description': description,
        'login' : login,
        'email' : email,
        'url' : url,
        'icon': icon,
        'anyone_can_modify': 0 if anyone_can_modify == True else 1,
        }

#    print(token)
    r = httpx.post(url, params=payload, headers= { "Authorization": token })
    return json.loads(r.text)

username = 'jaro'
password = 'XXX'
apikey = 'XXX'


token = teampass_api_key(username, password, apikey)
response = create_item(token,'test',2,'$145678Pass*','test login','login','jaro@xxx.xx','https://tp.xx.xxx.xx')
print(response)

And then response is:

{'error': 'Access denied'}

And logs is:

172.16.254.245 - - [10/Jan/2025:12:40:36 +0100] "POST /api/index.php/item/create?label=test&folder_id=2&password=%24145678Heslo%2A&description=test%20login&login=login&email=jaro%40xxx.xx&url=https%3A%2F%2Ftp.xx.xxx.xx%2Fapi%2Findex.php%2Fitem%2Fcreate&icon=fa-solid%20fa-start&anyone_can_modify=0 HTTP/1.1" 404 36 "-" "python-httpx/0.26.0"

Second problem:
We discovered due API design (despite commands using POST method, passwords are shown as GET parameters) confidential passwords are leaking into webserver+teampass logs (nginx, apache, php-fpm). Could this be reworked to use POST method?

172.16.254.245 - - [10/Jan/2025:12:40:36 +0100] "POST /api/index.php/authorize?apikey=VsCBvrCQCqBkZTz9pCBgKk7E589Q3zytygMjakM&login=jaro&password=XXXXXXX HTTP/1.1" 200 7952 "-" "python-httpx/0.26.0"

Expected behaviour

Passwords are not shown in logs

Actual behaviour

Passwords are shown in logs

Server configuration

Operating system:
Rocky Linux 9.4

Web server:
nginx/1.20.1

Database:
mariadb-10.5.22-1.el9_2.x86_64

PHP version:
php82-runtime-8.2-5.el9.remi.x86_64

Teampass version:
3.1.3.8

Teampass configuration file:

<?php
// DATABASE connexion parameters
define("DB_HOST", "localhost");
define("DB_USER", "teampass_admin");
define("DB_PASSWD", "XXXX");
define("DB_NAME", "teampass");
define("DB_PREFIX", "teampass_");
define("DB_PORT", "3306");
define("DB_ENCODING", "utf8");
define("DB_SSL", false); // if DB over SSL then comment this line
// if DB over SSL then uncomment the following lines
//define("DB_SSL", array(
//    "key" => "",
//    "cert" => "",
//    "ca_cert" => "",
//    "ca_path" => "",
//    "cipher" => ""
//));
define("DB_CONNECT_OPTIONS", array(
    MYSQLI_OPT_CONNECT_TIMEOUT => 10
));
define("SECUREPATH", "/var/teampass");
define("SECUREFILE", "XXXX");

if (isset($_SESSION['settings']['timezone']) === true) {
    date_default_timezone_set($_SESSION['settings']['timezone']);
}

Client configuration

Browser:
python-httpx/0.26.0

Operating system:
Rocky Linux 9.4

Logs

Web server error log

See above

Log from the web-browser developer console (CTRL + SHIFT + i)

N/A
@nilsteampassnet
Copy link
Owner

nilsteampassnet commented Jan 12, 2025

@skt-jdrzik
I cannot help with the 1st point as I'm not very good in Python.
Regarding

We discovered due API design (despite commands using POST method, passwords are shown as GET parameters) confidential passwords are leaking into webserver+teampass logs (nginx, apache, php-fpm). Could this be reworked to use POST method?

The API auth part is already performed using a POST and the parameters are passed into the BOBY as it is recommended for security good practises.
Here your issue comes from the fact that you are using httpx with params (which sends the paylod to the url) instead of json that sends the payload to the body).

So you should write : r = httpx.post(url, json=payload)

This should do the trick.

@skt-jdrzik
Copy link
Author

@nilsteampassnet
If I try update my script to post data as json, I am facing another problem with it...
Here is the output of script:

{"error":"AuthModel::getUserAuth(): Argument #1 ($login) must be of type string, null given, called in \/var\/www\/html\/teampass\/api\/Controller\/Api\/Auth
Controller.php on line 44 Something went wrong! Please contact support.2"}

Same happens with curl:

curl -vvv --header 'Content-Type: application/json' --request POST --data '{"apikey": "xxx", "login": "jaro", "password": "xxx"}' https://tp.xx.xxx.xx/api/index.php/authorize
...
< HTTP/2 500 
< server: nginx/1.20.1
< date: Mon, 13 Jan 2025 10:36:54 GMT
< content-type: application/json
< x-powered-by: PHP/8.2.22
< access-control-allow-origin: http://tp.xx.xxx.xxx
< access-control-allow-methods: POST, GET
< access-control-max-age: 3600
< access-control-allow-headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With
< cache-control: no-cache, must-revalidate
< 
* Connection #0 to host ***** left intact
{"error":"AuthModel::getUserAuth(): Argument #1 ($login) must be of type string, null given, called in \/var\/www\/html\/teampass\/api\/Controller\/Api\/AuthController.php on line 44 Something went wrong! Please contact support.2"}⏎                                                                                  

This is my nginx config for clarity:

...
   location ~ /.*\.php(/|$) {
        fastcgi_pass  php82-fpm;
        fastcgi_read_timeout 300;
 
        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param HTTPS off;
    }
...

@nilsteampassnet
Copy link
Owner

@skt-jdrzik
This means that the params are not sent correctly.
And it is quite coherant as the API is waiting for params in body not in header. As a consequence, the error "param is null" is thrown.
Perhaps, try to replace json by data.

You can also give a try using Postman, just to check if the call is correct from your side ;)

@skt-jdrzik
Copy link
Author

I got some new work assigments and I will not have much time to look deeper into it at the moment, maybe next week. Anyway I tried to debug teampass application and I think problem is in the teampass app code, which does not expect/handle json data in the request:
There is this isssue on stack overflow which documents that when sending raw json data using Content-Type: application/json the universal superglobal $_REQUEST will be null in PHP.
maybe I am mistaken, but when I check teampass code, this seems to be happening in api/Controller/Api/BaseController.php on your side...: :

    /**
     * Get querystring params.
     * 
     * @return array|string
     */
    public function getQueryStringParams()
    {
        $request = symfonyRequest::createFromGlobals();
        $queryString = $request->getQueryString();
        parse_str(html_entity_decode($queryString), $query);
        return $this->sanitizeUrl($query);
    }

@nilsteampassnet
Copy link
Owner

Have you tried with postman?
I'm using heavily the api and haven't this issue, so I'm really surprised.
Postman will give us a great information on where to investigate.

@skt-jdrzik
Copy link
Author

I have tried Boomerang - SOAP & REST client with same results...

{
  "error": "AuthModel::getUserAuth(): Argument #1 ($login) must be of type string, null given, called in /var/www/html/teampass/api/Controller/Api/AuthController.php on line 44 Something went wrong! Please contact support.2"
}

Maybe there is problem with nginx config, but on next week I will have more time to investigate it...

@skt-jdrzik
Copy link
Author

First problem can be solved by this fix:

    public function getQueryStringParams()
    {
        $request = symfonyRequest::createFromGlobals();
        $queryString = $request->getQueryString();
        if ($request->getContentType() != 'json' || !$request->getContent()) {
                parse_str(html_entity_decode($queryString), $query);
                return $this->sanitizeUrl($query);
        }

        return $request->toArray();
    }


@skt-jdrzik
Copy link
Author

skt-jdrzik commented Jan 14, 2025

Second problem is located in my script and there is a mistake in documentation which is missing information about tags parameter...
Final solution with script here:

import json

def teampass_api_key(username, password, apikey):
    url = "https://tp.xx.xxx.xx/api/index.php/authorize"
    payload = { 'apikey': apikey,
                'login': username,
                'password': password}
    print(payload)
    r = httpx.post(url, json=payload)
    print(r.url)
    response = json.loads(r.text)
    return response["token"]

def create_item(token,label,folder_id,password,description,login,email,url,tags,icon='fa-solid fa-start',anyone_can_modify=True):
    url = "https://tp.xx.xxx.xx/api/index.php/item/create"
    payload = {
        'label' : label,
        'folder_id': folder_id,
        'password': password,
        'description': description,
        'login' : login,
        'email' : email,
        'url' : url,
        'icon': icon,
        'tags': tags,
        'anyone_can_modify': 0 if anyone_can_modify == True else 1,
        }

#    print(token)
    r = httpx.post(url, params=payload, headers= { "Authorization": f"Bearer {token}" })
    return json.loads(r.text)

username = 'jaro'
password = 'XXX'
apikey = 'XXX'


token = teampass_api_key(username, password, apikey)
response = create_item(token,'test',2,'$145678Pass*','test login','login','jaro@xxx.xx','https://tp.xx.xxx.xx','api')
print(response)

@skt-mmisuth
Copy link

@nilsteampassnet it seems, that you are not reading POST body (eg. php://input), thus api call params are read only from urlencoded "GET params" of the given POST request. This causes password param leakage into the logs. @skt-jdrzik's fix seems to solve that issue.

Would you be willing to incorporate it into your API layer? And also, should it be incorporated, how long will it take to get into stable relase in your opinion?

@skt-mmisuth
Copy link

@nilsteampassnet ping?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants