Skip to content

Login fails with pdo session storage on PostgreSql #2067

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

Closed
ARS81 opened this issue Aug 31, 2011 · 10 comments
Closed

Login fails with pdo session storage on PostgreSql #2067

ARS81 opened this issue Aug 31, 2011 · 10 comments

Comments

@ARS81
Copy link

ARS81 commented Aug 31, 2011

Take an empty project.
Configure session handling to use pdo with postgresql.
Users coming from in_memory provider.
Login is handled by form_login.

Result:
When login form (/login) comes in for the first time, 1 session is created for the user.
After successful authentication, another session row is created in the session table and/but logged user is still anonymous.

With default (native) session handling everything is working fine.
With mysql the user is logged in, but gets a new session, instead of continuing with the existing one (is it normal?)

So the problem is that session handling with postgre it's unable to get the user logged in.
(the results were the same with using FOSUserBundle)

Symfony: 2.0.1
PHP: 5.3.3-7+squeeze3
Postgre: PostgreSQL 8.4.8
System: Linux dev 2.6.32-5-686 #1 SMP Mon Jun 13 04:13:06 UTC 2011 i686

# app/config/config.yml
imports:
    - { resource: parameters.ini }
    - { resource: security.yml }

framework:
    #esi:             ~
    translator:      { fallback: en }
    secret:          %secret%
    charset:         UTF-8
    router:          { resource: "%kernel.root_dir%/config/routing.yml" }
    form:            true
    csrf_protection: true
    validation:      { enable_annotations: true }
    templating:      { engines: ['twig'] } #assets_version: SomeVersionScheme
    session:
        storage_id:     session.storage.pdo

#Twig Configuration
twig:
    debug:            %kernel.debug%
    strict_variables: %kernel.debug%

#Assetic Configuration
assetic:
    debug:          %kernel.debug%
    use_controller: false
    filters:
        cssrewrite: ~
        # closure:
        #     jar: %kernel.root_dir%/java/compiler.jar
        # yui_css:
        #     jar: %kernel.root_dir%/java/yuicompressor-2.4.2.jar

#Doctrine Configuration
doctrine:
    dbal:
        driver:   %database_driver%
        host:     %database_host%
        port:     %database_port%
        dbname:   %database_name%
        user:     %database_user%
        password: %database_password%
        charset:  UTF8

    orm:
        auto_generate_proxy_classes: %kernel.debug%
        auto_mapping: true

#Swiftmailer Configuration
swiftmailer:
    transport: %mailer_transport%
    host:      %mailer_host%
    username:  %mailer_user%
    password:  %mailer_password%

jms_security_extra:
    secure_controllers:  true
    secure_all_services: false

parameters:
    pdo.db_options:
        db_table:    session
        db_id_col:   sess_id
        db_data_col: sess_data
        db_time_col: sess_time

services:
    pdo:
        class:    PDO
        arguments:
            dsn:      "pgsql:dbname=testdb"
            user:     testuser
            password: testpwd

    session.storage.pdo:
        class:     Symfony\Component\HttpFoundation\SessionStorage\PdoSessionStorage
        arguments: [@pdo, %session.storage.options%, %pdo.db_options%]
# app/config/security.yml
security:
    encoders:
        Symfony\Component\Security\Core\User\User: plaintext

    providers:
        in_memory:
            users:
                mtamas:  { password: akarmi, roles: 'ROLE_USER' }
                admin: { password: admin, roles: 'ROLE_ADMIN' }

    firewalls:
        main:
            pattern:    ^/
            anonymous:  true
            form_login:
                check_path: /checklogin
                login_path: /login
                username_parameter: username
                password_parameter: password
                post_only: true
                always_use_default_target_path: true
                default_target_path: /teszt
            logout:
                path:   /logout
                target: /

    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: ROLE_ADMIN
@lsmith77
Copy link
Contributor

lsmith77 commented Sep 7, 2011

do you have any errors in postgresql logs?

@ARS81
Copy link
Author

ARS81 commented Sep 7, 2011

No, there were no errors.
Here is the log of all executed queries:
(started with emtpy sessions table)
(this time users are from users table, not from in_memory provieder but it's doesn't matter)

After calling "/login" action:

execute pdo_stmt_00000001: SELECT sess_data FROM session WHERE sess_id = $1
parameters: $1 = 'b5qplnngm3ecc4tttvihecelo1'

execute pdo_stmt_00000002: INSERT INTO session (sess_id, sess_data, sess_time) VALUES ($1, $2, $3)
parameters: $1 = 'b5qplnngm3ecc4tttvihecelo1', $2 = '', $3 = '1315384577'

execute pdo_stmt_00000003: UPDATE session SET sess_data = $1, sess_time = $2 WHERE sess_id = $3
parameters: $1 = '_symfony2|a:3:{s:10:"attributes";a:0:{}s:7:"flashes";a:0:{}s:6:"locale";s:2:"en";}', $2 = '1315384577', $3 = 'b5qplnngm3ecc4tttvihecelo1'

execute pdo_stmt_00000001: SELECT sess_data FROM session WHERE sess_id = $1
parameters: $1 = 'b5qplnngm3ecc4tttvihecelo1'

execute pdo_stmt_00000002: UPDATE session SET sess_data = $1, sess_time = $2 WHERE sess_id = $3
parameters: $1 = '_symfony2|a:3:{s:10:"attributes";a:0:{}s:7:"flashes";a:0:{}s:6:"locale";s:2:"en";}', $2 = '1315384578', $3 = 'b5qplnngm3ecc4tttvihecelo1'

Now there is 1 row in session table.

After entering the correct username and password and than pressing login button:

execute pdo_stmt_00000001: SELECT sess_data FROM session WHERE sess_id = $1
parameters: $1 = 'b5qplnngm3ecc4tttvihecelo1'

execute pdo_stmt_00000001: SELECT t0.id AS id1, t0.username AS username2, t0.password AS password3 FROM users t0 WHERE t0.username = $1
parameters: $1 = 'mtamas'

execute pdo_stmt_00000002: UPDATE session SET sess_data = $1, sess_time = $2 WHERE sess_id = $3
parameters: $1 = '_symfony2|a:3:{s:10:"attributes";a:1:{s:14:"_security_main";s:451:"C:74:"Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken":363:{a:3:{i:0;N;i:1;s:4:"main";i:2;s:323:"a:4:{i:0;O:22:"UserBundle\Entity\User":4:{s:5:"', $2 = '1315384693', $3 = 'r8s2c5p8npblbl0rq2dbicv026'

execute pdo_stmt_00000003: INSERT INTO session (sess_id, sess_data, sess_time) VALUES ($1, $2, $3)
parameters: $1 = 'r8s2c5p8npblbl0rq2dbicv026', $2 = '_symfony2|a:3:{s:10:"attributes";a:1:{s:14:"_security_main";s:451:"C:74:"Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken":363:{a:3:{i:0;N;i:1;s:4:"main";i:2;s:323:"a:4:{i:0;O:22:"UserBundle\Entity\User":4:{s:5:"', $3 = '1315384693'

execute pdo_stmt_00000001: SELECT sess_data FROM session WHERE sess_id = $1
parameters: $1 = 'r8s2c5p8npblbl0rq2dbicv026'

execute pdo_stmt_00000002: UPDATE session SET sess_data = $1, sess_time = $2 WHERE sess_id = $3
parameters: $1 = '_symfony2|a:3:{s:10:"attributes";a:0:{}s:7:"flashes";a:0:{}s:6:"locale";s:2:"en";}', $2 = '1315384693', $3 = 'r8s2c5p8npblbl0rq2dbicv026'

execute pdo_stmt_00000001: SELECT sess_data FROM session WHERE sess_id = $1
parameters: $1 = 'r8s2c5p8npblbl0rq2dbicv026'

execute pdo_stmt_00000002: UPDATE session SET sess_data = $1, sess_time = $2 WHERE sess_id = $3
parameters: $1 = '_symfony2|a:3:{s:10:"attributes";a:0:{}s:7:"flashes";a:0:{}s:6:"locale";s:2:"en";}', $2 = '1315384693', $3 = 'r8s2c5p8npblbl0rq2dbicv026'

Now there are 2 records in sessions table.
User is logged in, but smyfony tells we are anonymous.

@eventhorizonpl
Copy link

Hi,

It seems that I hit the same problem here. Any hint where to start looking for solution to the problem?

Best regards,
Michal

@eventhorizonpl
Copy link

Symfony\Component\HttpFoundation\SessionStorage\PdoSessionStorage sessionWrite($id, $data) gets a different $data for MySQL and PostgreSQL.

Here is $data for MySQL
array(1) { [0]=> array(1)
{ [0]=> string(975) "_symfony2|a:3:
{s:10:"attributes";a:1:
{s:14:"_security_main";s:862:"C:74:"Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken":774:
{a:3:
{i:0;N;i:1;s:4:"main";i:2;s:734:"a:4:
{i:0;C:34:"EventHorizon\CmsBundle\Entity\User":254:
{a:8:
{i:0;s:128:"4de821b9ad696f2240b58931afc2bf404924bbbd16d8880aba69fa6660d6ff58a2237d90aa5c4e77be377cbfeff000333906cec76c4345f41efb5af866a5d26d";i:1;s:31:"pc81iad8d40k4wo0wkg4440ggwc0wkg";i:2;s:5:"admin";i:3;s:5:"admin";i:4;b:0;i:5;b:0;i:6;b:0;i:7;b:1;}
}i:1;b:1;i:2;a:3:
{i:0;O:41:"Symfony\Component\Security\Core\Role\Role":1:
{s:47:"Symfony\Component\Security\Core\Role\Rolerole";s:10:"ROLE_ADMIN";}
i:1;O:41:"Symfony\Component\Security\Core\Role\Role":1:
{s:47:"Symfony\Component\Security\Core\Role\Rolerole";s:16:"ROLE_SUPER_ADMIN";}
i:2;O:41:"Symfony\Component\Security\Core\Role\Role":1:
{s:47:"Symfony\Component\Security\Core\Role\Rolerole";s:9:"ROLE_USER";}
}i:3;a:0:{}
}";
}
}";
}s:7:"flashes";a:0:{}s:6:"locale";s:2:"pl";
}"
}
}

Here is $data for PostgreSQL

array(1) { [0]=> array(1)
{ [0]=> string(581) "_symfony2|a:3:
{s:10:"attributes";a:1:
{s:14:"_security_main";s:862:"C:74:"Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken":774:
{a:3:
{i:0;N;i:1;s:4:"main";i:2;s:734:"a:4:
{i:0;C:34:"EventHorizon\CmsBundle\Entity\User":254:
{a:8:
{i:0;s:128:"005a222fd785ee393b34018419e238ce6523d2a6984dda5d0cb8613a3006cb64c0aad35e842363a0f6a451f830791d516efdccfaee02111118682c6a9146f92e";i:1;s:31:"ao08lrppo6wwwc44w8w4k80k0csooso";i:2;s:5:"admin";i:3;s:5:"admin";i:4;b:0;i:5;b:0;i:6;b:0;i:7;b:1;}
}i:1;b:1;i:2;a:3:
{i:0;O:41:"Symfony\Component\Security\Core\Role\Role":1:{s:47:"" }
}

$data for PostgreSQL appears to be incomplete.

@mvrhov
Copy link

mvrhov commented Oct 10, 2011

I've spent a couple of hours on this. Everything works if base64_encode the session data. Looking at the encoded session it seems, that there are some null characters in serialized session -> serialized data is not binary safe! blame that to PHP.
The only solutions I see is encode and decode session data with base64 and get a speed penalty for this. checking for the type of the column we are storing into would be even slower. I also really doubt that documenting that data column should be of blob type is going to help. BTW the same issue should apply for the doctrine based session storage.
/cc @fabpot

@schmittjoh
Copy link
Contributor

Do you use a PHP version < 5.3.6 ?

@eventhorizonpl
Copy link

I'm using php PHP 5.3.8

@mvrhov
Copy link

mvrhov commented Oct 10, 2011

No it's 5.3.8. And it won't work please read this http://www.postgresql.org/docs/8.3/interactive/libpq-exec.html#LIBPQ-EXEC-ESCAPE-STRING. on the postgresql side.
And following excerpt from php serialize manual:
Note:

Object's private members have the class name prepended to the member name; protected members have a '*' prepended to the member name. These prepended values have null bytes on either side.

P.S. Let's add to this that some smart admin might have set up a igbinary as a default serializer and then everything falls apart.

@fabpot
Copy link
Member

fabpot commented Oct 11, 2011

We had the same issue when storing profiles into the profiler. We have fixed it by base64 encoding after serialization (base64_encode(serialize($profile)) and unserialize(base64_decode($data))). We should probably do the same for the PDO session storage.

@mvrhov
Copy link

mvrhov commented Oct 11, 2011

Ok, PR coming.

@fabpot fabpot closed this as completed in 94e7e54 Oct 25, 2011
fabpot added a commit that referenced this issue Oct 25, 2011
Commits
-------

0907111 session data needs to be encoded because it can contain non binary safe characters e.g null. Fixes #2067

Discussion
----------

session data needs to be encoded because it can contain non binary safe characters e.g null., part 2

Bug fix: yes
Feature addition: no
Backwards compatibility break: yes
Symfony2 tests pass: yes
Fixes the following tickets: #2067

I'm marking this as a compatibility break because session table should be cleared and even if not cleared all currently logged in users will be logged out.

This is the fix for a same issue in DBAL session storage made against master.

---------------------------------------------------------------------------

by schmittjoh at 2011/10/12 02:44:19 -0700

If I understand this correctly, only the PgSqlPlatform is affected by this. What do you think about adding an ``ìnstanceof PgSqlPlatform`` check?

---------------------------------------------------------------------------

by mvrhov at 2011/10/12 03:47:52 -0700

It's the same for sqlite, it just happens that mysql escapes \0, so we can say it's driver dependent.
The Drupal guys had the same issue http://drupal.org/node/690746 , they changed to column type to bytea for pgsql and for mysql to blob, also in Drupal report you can find that storing this into a session hash_file('md5', 'CHANGELOG.txt', TRUE) will trigger the similar problem in mysql.
The other thing to consider is what I mentioned in original bugreport, e.g igbinary as default serializer for session data.
fabpot added a commit that referenced this issue Oct 3, 2014
This PR was merged into the 2.6-dev branch.

Discussion
----------

[HttpFoundation] enhance PdoSessionHandler

| Q             | A
| ------------- | ---
| Bug fix?      | yes
| New feature?  | yes
| BC breaks?    | yes
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #5483, #2067, #2382, #9029
| License       | MIT

0. [x] Continuation of locking implementation (#10908): Implement different locking strategies
  - `PdoSessionHandler::LOCK_TRANSACTIONAL` (default): Issues a real row lock but requires a transaction
  - `PdoSessionHandler::LOCK_ADVISORY`: app-level lock, safe as long as only the PdoSessionHandler accesses sessions, advantage is it does not require a transaction (not implemented for oracle or sqlsrv yet)
  - `PdoSessionHandler::LOCK_NONE`: basically what is was before, prone to race conditions, means the last session write wins

1. [x] Save session data as binary: Encoding session data was definitely the wrong solution. Session data is binary text (esp. when using other session.serialize_handler) that must stay as-is and thus must also be safed in a binary column. Base64 encoding session data just decreses performance and increases storage costs and is semantically wrong because it does not have a character encoding.
That saving null bytes in Posgres won't work on a character column is also documented

    > First, binary strings specifically allow storing octets of value zero and other "non-printable" octets (usually, octets outside the range 32 to 126). Character strings disallow zero octets, and also disallow any other octet values and sequences of octet values that are invalid according to the database's selected character set encoding.
http://www.postgresql.org/docs/9.1/static/datatype-binary.html#DATATYPE-BINARY-TABLE

2. [x] Implement lazy connections that are only opened when session is used by either passing a dsn string explicitly or falling back to session.save_path ini setting. Fixes #9029

3. [x] add a create table method that creates the correct table depending on database vendor. This makes the class self-documenting and standalone useable.

5. [x] add lifetime column to session table which allows to have different lifetimes for each session

6. [x] add isSessionExpired() method to be able to distinguish between a new session and one that expired due to inactivity, e.g. to display flash message to user

7. [x] added upgrade and changelog notes

Commits
-------

1bc6680 [HttpFoundation] implement different locking strategies for sessions
6f5748e adjust sqlite table definition
5978fcf added upgrade and changelog notes for PdoSessionHandler
182a5d3 [HttpFoundation] add create table method to pdo session handler
e79229d [HttpFoundation] allow different lifetime per session
af1bb1f add test for null byte in session data
251238d [HttpFoundation] implement lazy connect for pdo session handler
7dad54c [HttpFoundation] remove base64 encoding of session data
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants