CVE-2021-25940 (C++, published 2021-11-16T10:15Z)
Description: value: In ArangoDB, versions v3.7.6 through v3.8.3 are vulnerable to Insufficient Session Expiration. When a user’s password is changed by the administrator, the session isn’t invalidated, allowing a malicious user to still be logged in and perform arbitrary actions within the system.
Severity: MEDIUM (CVSS v3 base score: 8)
Weakness: CWE-613 \u2014 Insufficient Session Expiration
According to WASC, "Insufficient Session Expiration is when a web site permits an attacker to reuse old session credentials or session IDs for authorization."
Repository: https://github.com/arangodb/arangodb
Files changed:
CHANGELOG
arangod/GeneralServer/AuthenticationFeature.cpp
arangod/GeneralServer/AuthenticationFeature.h
arangod/RestHandler/RestAuthHandler.cpp
arangod/RestHandler/RestAuthHandler.h
arangod/RestServer/ServerFeature.cpp
tests/js/client/server_parameters/test-server-session-timeout.js
Commit message: Revive startup parameter `--server.session-timeout` (#14118)

Co-authored-by: Tobias Gödderz <tobias@arangodb.com>

Vulnerable code:
      _authenticationTimeout(0.0) {
                     new BooleanParameter(&_localAuthentication));
                     new BooleanParameter(&_authenticationUnixSockets));
  // Maybe deprecate this option in devel
#include "Ssl/SslInterface.h"
    : RestVocbaseBaseHandler(server, request, response),
      _validFor(60 * 60 * 24 * 30) {}

std::string RestAuthHandler::generateJwt(std::string const& username,
                                         std::string const& password) {
  AuthenticationFeature* af = AuthenticationFeature::instance();
  TRI_ASSERT(af != nullptr);
  return fuerte::jwt::generateUserToken(af->tokenCache().jwtSecret(), username, _validFor);
}
  _username = usernameSlice.copyString();
  } else if (um->checkPassword(_username, password)) {
      std::string jwt = generateJwt(_username, password);
      resultBuilder.add("jwt", VPackValue(jwt));
    _isValid = true;

void RestAuthHandler::shutdownExecute(bool isFinalized) noexcept {
  try {
    if (_isValid) {
      events::LoggedIn(*_request, _username);
    } else {
      events::CredentialsBad(*_request, _username);
    }
  } catch (...) {
  }
  RestVocbaseBaseHandler::shutdownExecute(isFinalized);
}
  std::string generateJwt(std::string const&, std::string const&);

  void shutdownExecute(bool isFinalized) noexcept override;

 private:
  std::string _username;
  bool _isValid = false;
  std::chrono::seconds _validFor;
  options->addObsoleteOption(
      "--server.session-timeout",
      "timeout of web interface server sessions (in seconds)", true);


Fixed code:
* Revive startup parameter `--server.session-timeout` to control the timeout
  for web interface sessions and other sessions that are based on JWTs created
  by the `/_open/auth` API.
  
  This PR also changes the default session timeout for web interface sessions
  to one hour. Older versions of ArangoDB had longer session timeouts.

      _authenticationTimeout(0.0),
      _sessionTimeout(static_cast<double>(1 * std::chrono::hours(1) / std::chrono::seconds(1))) { // 1 hour
  
  options->addOption("--server.session-timeout",
                     "timeout in seconds for web interface JWT sessions",
                     new DoubleParameter(&_sessionTimeout),
                     arangodb::options::makeFlags(
                       arangodb::options::Flags::DefaultNoComponents,
                       arangodb::options::Flags::OnCoordinator,
                       arangodb::options::Flags::OnSingle))
                     .setIntroducedIn(30900);
                     new BooleanParameter(&_localAuthentication),
                     arangodb::options::makeFlags(
                       arangodb::options::Flags::DefaultNoComponents,
                       arangodb::options::Flags::OnCoordinator,
                       arangodb::options::Flags::OnSingle));
                     new BooleanParameter(&_authenticationUnixSockets),
                     arangodb::options::makeFlags(
                       arangodb::options::Flags::DefaultNoOs,
                       arangodb::options::Flags::OsLinux,
                       arangodb::options::Flags::OsMac));
  
  if (_sessionTimeout <= 1.0) {
    LOG_TOPIC("85046", FATAL, arangodb::Logger::AUTHENTICATION)
        << "--server.session-timeout has an invalid value: " << _sessionTimeout;
    FATAL_ERROR_EXIT();
  }

  double sessionTimeout() const { return _sessionTimeout; }
  double _sessionTimeout;
#include "Basics/ScopeGuard.h"
    : RestVocbaseBaseHandler(server, request, response) {}
  std::string const username = usernameSlice.copyString();
  bool isValid = false;

  auto guard = scopeGuard([&]() {
    try {
      if (isValid) {
        events::LoggedIn(*_request, username);
      } else {
        events::CredentialsBad(*_request, username);
      }
    } catch (...) {
      // nothing we can do
    }
  });
  
  } else if (um->checkPassword(username, password)) {
      resultBuilder.add("jwt", VPackValue(generateJwt(username)));
    isValid = true;
std::string RestAuthHandler::generateJwt(std::string const& username) const {
  AuthenticationFeature* af = AuthenticationFeature::instance();
  TRI_ASSERT(af != nullptr);
  return fuerte::jwt::generateUserToken(
      af->tokenCache().jwtSecret(), 
      username, 
      std::chrono::seconds(uint64_t(af->sessionTimeout())));
}

  std::string generateJwt(std::string const& username) const;
/*jshint globalstrict:false, strict:false */
/* global getOptions, assertEqual, arango */

////////////////////////////////////////////////////////////////////////////////
/// @brief test for server parameters
///
/// DISCLAIMER
///
/// Copyright 2010-2012 triagens GmbH, Cologne, Germany
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
///     http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
/// Copyright holder is ArangoDB Inc, Cologne, Germany
///
/// @author Jan Steemann
/// @author Copyright 2019, ArangoDB Inc, Cologne, Germany
////////////////////////////////////////////////////////////////////////////////

if (getOptions === true) {
  return {
    'server.session-timeout': '5',
    'server.authentication': 'true',
    'server.jwt-secret': 'haxxmann',
  };
}
const jsunity = require('jsunity');
const request = require('@arangodb/request');

function testSuite() {
  let baseUrl = function () {
    return arango.getEndpoint().replace(/^tcp:/, 'http:').replace(/^ssl:/, 'https:');
  };

  return {
    testSessionTimeout: function() {
      let result = request.get(baseUrl() + "/_api/version");
      // no access
      assertEqual(401, result.statusCode);

      result = request.post({
        url: baseUrl() + "/_open/auth", 
        body: {
          username: "root",
          password: ""
        },
        json: true
      });

      assertEqual(200, result.statusCode);
      const jwt = result.json.jwt;
      
      result = request.get({
        url: baseUrl() + "/_api/version",
        auth: {
          bearer: jwt,
        }
      });

      // access granted
      assertEqual(200, result.statusCode);

      require("internal").sleep(7);

      result = request.get({
        url: baseUrl() + "/_api/version",
        auth: {
          bearer: jwt,
        }
      });

      // JWT expired
      assertEqual(401, result.statusCode);
    },
  };
}

jsunity.run(testSuite);
return jsunity.done();