Security Advisories (1)
CVE-2026-19349 (2026-08-16)

Lemonldap::NG::Portal versions from 2.0.0 before 2.16.9, from 2.17.0 before 2.21.5, from 2.22.0 before 2.23.3 for Perl allow authentication bypass via an OAuth2 state parameter stored as an SSO session in the GitHub and LinkedIn backends. Before redirecting to the identity provider, extractFormInfo() creates the state session with the positional call `getApacheSession( undef, 1, 0, 'GitHubState' )`. getApacheSession() takes a session id followed by a named argument hash, so the trailing arguments become that hash, `kind` defaults to SSO, and the state is written to the global session storage as a regular SSO session. Its identifier is handed to the unauthenticated visitor as the state parameter of the redirection URL. Any visitor who reaches the GitHub or LinkedIn endpoint can replay that identifier as a session cookie and obtain a valid SSO session without authenticating. The session holds neither _user nor authenticationLevel, which the shipped bootstrap configuration accepts because it grants virtual hosts a "default => accept" access rule; deployments whose rules test the user or require an authentication level are less exposed. Only configurations with the GitHub or LinkedIn authentication module enabled are affected.

NAME

test-lib.pm - Test framework for LLNG portal

SYNOPSIS

use Test::More;
use strict;
use IO::String;

require 't/test-lib.pm';

my $res;

my $client = LLNG::Manager::Test->new( {
    ini => {
        logLevel => 'error',
        #...
    }
  }
);

ok(
    $res = $client->_post(
        '/',
        IO::String->new('user=dwho&password=dwho'),
        length => 23
    ),
    'Auth query'
);
count(1);
expectOK($res);
my $id = expectCookie($res);

clean_sessions();
done_testing( count() );

DESCRIPTION

This test library permits one to simulate browser navigation.

Functions

In these functions, $res is the result of a LLNG::Manager::Test::_get() or LLNG::Manager::Test::_post() call (see below).

count($inc)

Returns number of tests done. Increment test number if an argument is given

buildForm($params)

Convenience method that builds a url-encoded query string from a hash of arguments

explain( $result, $expected_result )

Used to display error if test fails:

ok( $res->[0] == 302, 'Get redirection' ) or
  explain( $res->[0], 302 );

reset_tmpdir()

Reinitialize temp directory, useful for cleaning state when multiple subtests may interfere with each other

clean_sessions()

Clean sessions created during tests

expectRedirection( $res, $location )

Verify that request result is a redirection to $location. $location can be:

a string: location must match exactly
a regexp: location must match this regexp. In this case, the list of matching strings are returned. Example:
my( $uri, $query ) = expectRedirection( $res, qr#http://host(/[^\?]*)?(.*)$# );

expectAutoPost(@args)

Same behaviour as expectForm() but verify also that form method is post.

TODO: verify javascript

expectForm( $res, $hostRe, $uriRe, @requiredFields )

Verify form in HTML result and return ( $host, $uri, $query, $method ):

verify that a GET/POST form exists
if a $hostRe regexp is given, verify that form target matches and populates $host. Skipped if $hostRe eq "#"
if a $uriRe regexp is given, verify that form target matches and populates $uri
if @requiredFields exists, verify that each element is an input name
build form-url-encoded string looking at parameters/values and store it in $query

expectAuthenticatedAs($res, $user)

Verify that result has a Lm-Remote-User header and value is $user

expectSessionAttributes($app,$id,%attributes)

Verify that the session contains attributes with these values

expectOK($res)

Verify that returned code is 200

expectJSON($res)

Verify that the HTTP response contains valid JSON and returns the corresponding object

expectForbidden($res)

Verify that returned code is 403.

expectBadRequest($res)

Verify that returned code is 400. Note that it works only for Ajax request (see below).

expectPortalError( $res, $errnum )

Verify that an error is displayed on the portal

expectReject( $res, $status, $code )

Verify that returned code is 401 (or $status) and JSON result contains error:"$code". Note that it works only for Ajax request (see below).

expectCookie( $res, $cookieName )

Check if a Set-Cookie exists and set a cookie named $cookieName. Return its value.

expectPdata( $res );

Check if the pdata cookie exists and returns its deserialized value.

exceptCspFormOK( $res, $host )

Verify that Content-Security-Policy header allows one to connect to $host.

getCookies($res)

Returns an hash ref with names => values of cookies set by server.

getHeader( $res, $hname )

Returns value of first header named $hname in $res response.

getRedirection($res)

Returns value of Location header.

getUser($res)

Returns value of Lm-Remote-User header.

tempdb

Return a temporary file named XXXX.db

register

Multi-handler system. This automatically takes care of loading the correct handler when processing a request.

withHandler

This method lets you run handler methods (such as conf reload) inside a give context

encodeUrl( $url );

Encode URL like the handler would, see ::Handler::Main

LLNG::Manager::Test Class

Accessors

app: built application
class: class to test (default Lemonldap::NG::Portal::Main)
p: portal object
ini: initialization parameters ($defaultIni values + given parameters)

Methods

login($client, $uid, $getParams) Do a simple login using the Demo backend

logout($id)

Launch a /?logout=1 request an test:

if response is 200

_get( $path, %args )

Simulates a GET requests to $path. Accepted arguments:

accept: accepted content, default to Ajax request. Use 'text/html' to test content (to launch a expectForm() for example).
custom: additional headers (hash ref only)
ip: remote address. Default to 127.0.0.1
method: default to GET. Only GET/DELETE values are acceptable (use _post() if you want to launch a POST/PUT request)
query: query string
referer
remote_user: REMOTE_USER header value

_post( $path, $body, %args )

Same as _get except that a body is required. $body must be a file handle. Example with IO::String:

ok(
    $res = $client->_post(
        '/',
        IO::String->new('user=dwho&password=dwho'),
        length => 23
    ),
    'Auth query'
);

_delete( $path, %args )

Call _get() with method set to DELETE.

_options( $path, %args )

Call _get() with method set to OPTIONS.

_put( $path, $body, %args )

Call _post() with method set to PUT