Problem/Motivation

Follow-up from #2238087: Rebase SessionManager onto Symfony NativeSessionStorage and #2245003: Use a random seed instead of the session_id for CSRF token generation.

Symfony provides a Session object that has a mechanism for handling data that would typically be stored in the $_SESSION super global variable. This mechanism is found in Symfony\Component\HttpFoundation\Session\Storage\MetadataBag . Using MetadataBag instead of the super global variable is recommended. Instead of having globally scoped variables (that make it difficult to use UnitTests to test functionality) we can have properly scoped variables.

This patch moves logic that involves handling Session variable storage to the Metadata object from the SessionManager.

In SessionManager
in regenerate() and isSessionObsolete()

two todos: .... the token seed
// can be moved onto Drupal\Core\Session\MetadataBag. The session manager
// then needs to notify the metadata bag when the token should be
// regenerated.

Proposed resolution

Move the token seed to the MetadataBag.

Remaining tasks

User interface changes

No.

API changes

?

Comments

znerol’s picture

Status: Active » Needs review
StatusFileSize
new11.93 KB

Status: Needs review » Needs work

The last submitted patch, 1: 2256257-move-token-seed-to-metadata-bag.diff, failed testing.

cosmicdreams’s picture

Issue summary: View changes
znerol’s picture

znerol’s picture

Instead of mocking CsrfTokenGenerator (the initial goal of the issue referenced in#4), it also would be possible to fix the installer/update script with #2272987: Do not persist session manager.

znerol’s picture

Status: Needs work » Needs review
StatusFileSize
new14.99 KB
new3.72 KB

Active again after #2272987: Do not persist session manager. Now that CsrfTokenGenerator stops accessing $_SESSION directly, code trying to use tokens for anonymous users really needs to make sure that there is a session before attempting to generate a token.

znerol’s picture

Reroll.

andypost’s picture

RTBC except nits

  1. +++ b/core/lib/Drupal/Core/Batch/BatchStorage.php
    @@ -8,25 +8,57 @@
    +   * The Database connection.
    

    s/Database/database

  2. +++ b/core/lib/Drupal/Core/Session/MetadataBag.php
    @@ -26,4 +28,33 @@ public function __construct(Settings $settings) {
    +   * Get the CSRF token seed.
    +   *
    +   * @return string
    +   *   The per-session CSRF token seed.
    +   */
    +  public function getCsrfTokenSeed() {
    +    if (isset($this->meta[self::CSRF_TOKEN_SEED])) {
    +      return $this->meta[self::CSRF_TOKEN_SEED];
    +    }
    +  }
    

    string|null
    when no seed stored

znerol’s picture

StatusFileSize
new14.79 KB
new1.42 KB

Thanks.

andypost’s picture

Status: Needs review » Reviewed & tested by the community

nice

neclimdul’s picture

+++ b/core/tests/Drupal/Tests/Core/Access/CsrfTokenGeneratorTest.php
@@ -35,20 +36,26 @@ class CsrfTokenGeneratorTest extends UnitTestCase {
+   * The mock session metadata bag.
+   *
+   * @var \Drupal\Core\Session\MetadataBag|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $sessionMetadata;
+
+  /**
...
+    $this->sessionMetadata = $this->getMockBuilder('Drupal\Core\Session\MetadataBag')
+      ->disableOriginalConstructor()
+      ->getMock();

@@ -56,27 +63,71 @@ protected function setUp() {
+  /**
+   * Set up default expectations on the mocks.
+   */
+  protected function setupDefaultExpectations() {
+    $key = Crypt::randomBytesBase64();
+    $this->privateKey->expects($this->any())
+      ->method('get')
+      ->will($this->returnValue($key));
+
+    $seed = Crypt::randomBytesBase64();
+    $this->sessionMetadata->expects($this->any())
+      ->method('getCsrfTokenSeed')
+      ->will($this->returnValue($seed));

Why are we going through all this trouble to mock the object? Can't we just use an instantiated version of the object?

neclimdul’s picture

NM, its because settings isn't test friendly. Opened #2329817: Add a Settings storage object

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 9: 2256257-move-token-seed-to-metadata-bag-9.diff, failed testing.

neclimdul’s picture

Status: Needs work » Reviewed & tested by the community

I guess the retest passed? Going to move this back to RTBC.

cosmicdreams’s picture

Status: Reviewed & tested by the community » Needs work

patch applies +!

  1. +++ b/core/lib/Drupal/Core/Access/CsrfTokenGenerator.php
    @@ -27,13 +27,21 @@ class CsrfTokenGenerator {
    +  public function __construct(PrivateKey $private_key, MetadataBag $session_metadata) {
    

    Please update Docblock

  2. +++ b/core/lib/Drupal/Core/Batch/BatchStorage.php
    @@ -66,14 +98,17 @@ public function cleanup() {
    +
    

    Extra needless whitespace

  3. +++ b/core/lib/Drupal/Core/Session/MetadataBag.php
    @@ -26,4 +28,33 @@ public function __construct(Settings $settings) {
    +  /**
    +   * Set the CSRF token seed.
    +   *
    +   * @param string $csrf_token_seed
    +   *   The per-session CSRF token seed.
    +   */
    +  public function setCsrfTokenSeed($csrf_token_seed) {
    +    $this->meta[self::CSRF_TOKEN_SEED] = $csrf_token_seed;
    +  }
    +
    +  /**
    +   * Get the CSRF token seed.
    +   *
    +   * @return string|null
    +   *   The per-session CSRF token seed or null when no value is set.
    +   */
    +  public function getCsrfTokenSeed() {
    +    if (isset($this->meta[self::CSRF_TOKEN_SEED])) {
    +      return $this->meta[self::CSRF_TOKEN_SEED];
    +    }
    +  }
    +
    +  /**
    +   * Clear the CSRF token seed.
    +   */
    +  public function clearCsrfTokenSeed() {
    +    unset($this->meta[self::CSRF_TOKEN_SEED]);
    +  }
    +
    

    if this proves to be important we should consider contributing this work back to Symfony

Crell’s picture

  1. +++ b/core/lib/Drupal/Core/Session/MetadataBag.php
    @@ -15,6 +15,8 @@
    +  const CSRF_TOKEN_SEED = 's';
    

    Er. How is a hard-coded token seed that cannot vary site to site in any way secure?

  2. +++ b/core/lib/Drupal/Core/Session/MetadataBag.php
    @@ -26,4 +28,33 @@ public function __construct(Settings $settings) {
    +    if (isset($this->meta[self::CSRF_TOKEN_SEED])) {
    

    Assuming the constant is kept, there's extremely few reasons to use self:: anymore. Use static:: instead, by default.

znerol’s picture

Status: Needs work » Needs review
StatusFileSize
new14.88 KB
new628 bytes

#16.1: done, thanks, #16.2: This is by intention. It is the single newline separating the last method from the closing brace of the class.

#16.3 Symfony has Symfony\Component\Security\Csrf which provides anything anyone ever could wish including a SessionStorage class. Because we do not have the Session object yet in Drupal we cannot just switch to those classes. We might want to revisit that later but at the moment the top priority for me is to advance the conversion of the session component. This patch is important especially because of the changes in the batch-system.

cosmicdreams’s picture

StatusFileSize
new14.89 KB
new1.18 KB

@Crell

17.1: Look closer, const CSRF_TOKEN_SEED isn't the seed, it's the array key. The value is created elsewhere and presumeably is random enough. Do we need the array key to be randomized as well?

Here's the change from self:: to static::

neclimdul’s picture

I spent a good bit of time looking into this code too. It confused me at first but cosmicdreams nailed it.

Other concerns had and my internal resolution.

  1. 's' doesn't really have much to do with csrf and 'c' would be better.
    • 'c' is used for cookie. there really isn't a good character so 's' is as good as any other.
  2. We don't really use single characters for constant strings like this, CSRF would be better.
    • This isn't done for code effeciency but because this metadata has to be stored in session storage. Smaller concise serialized objects in storage is at least a better reason then key length in code execution.
    • The internal implementation is not exposed other then through the serialized structure so it doesn't really matter.
cosmicdreams’s picture

@neclimdul so RTBC? (I can no longer mark it as RTBC myself)

dawehner’s picture

This issue is a good step forward.

  1. +++ b/core/lib/Drupal/Core/Access/CsrfTokenGenerator.php
    @@ -56,11 +66,13 @@ public function __construct(PrivateKey $private_key) {
    +    $seed = $this->sessionMetadata->getCsrfTokenSeed();
    +    if (empty($seed)) {
    +      $seed = Crypt::randomBytesBase64();
    +      $this->sessionMetadata->setCsrfTokenSeed($seed);
         }
     
    

    Can't we put that logic into the getCsrfTokenSeed method? So generate it on the fly? But yeah I see the point that the csrf token generator should handle the creation of csrf token seeds

  2. +++ b/core/lib/Drupal/Core/Session/MetadataBag.php
    @@ -15,6 +15,8 @@
     
    +  const CSRF_TOKEN_SEED = 's';
    +
    

    Maybe we could document this and tell us, why we use 's' and not 'c' for exmaple

znerol’s picture

StatusFileSize
new429 bytes
new15.01 KB

#22.1: We cannot generate the seed in MetadataBag::getCsrfTokenSeed(). Otherwise we'd forcibly open a session even when only calling CsrfTokenGenerator::validate() for anonymous users.

#22.2: Added documentation for the token-seed. The reason we cannot use 'c' is that this is already taken by the parent Symfony MetadataBag for the creation time.

cosmicdreams’s picture

Status: Needs review » Reviewed & tested by the community

Yep that looks good to go.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

This looks really good. One small nit:

+++ b/core/lib/Drupal/Core/Session/SessionManager.php
@@ -83,14 +82,13 @@ class SessionManager extends NativeSessionStorage implements SessionManagerInter
-   * @param \Symfony\Component\HttpFoundation\Session\Storage\MetadataBag $metadata_bag
+   * @param \Drupal\Core\Session\MetadataBag $metadata_bag
...
-  public function __construct(RequestStack $request_stack, Connection $connection, SymfonyMetadataBag $metadata_bag, Settings $settings) {
+  public function __construct(RequestStack $request_stack, Connection $connection, MetadataBag $metadata_bag, Settings $settings) {

Let's typehint on the interface SessionBagInterface

neclimdul’s picture

Status: Needs work » Reviewed & tested by the community

That would be incorrect.

The only reference in the constructor is

    parent::__construct($options, $write_check_handler, $metadata_bag);

and the parent sig is:

public function __construct(array $options = array(), $handler = null, MetadataBag $metaBag = null)
neclimdul’s picture

More information, those signatures a misleading as they're different bags. Our sig is our MetadataBag and the parent is Symfony's bag. We internally require methods specifically on our MetadataBag though. Specifically the CsrfToken Methods currently. Only matching the interface would not require this.

alexpott’s picture

Title: Move token seed in SessionManager in isSessionObsolete() and regenerate() to the MetadataBag » Move token seed in SessionManager in isSessionObsolete() and regenerate() to the MetadataBag
Status: Reviewed & tested by the community » Fixed

Discussed with @neclimdul on IRC - I pointed out our metadata bag is service and therefore swappable which means that we perhaps we should be using an interface here - but we agreed that in reality you could just extend from \Drupal\Core\Session\MetadataBag

Committed 0a12d85 and pushed to 8.0.x. Thanks!

  • alexpott committed 0a12d85 on 8.0.x
    Issue #2256257 by znerol, cosmicdreams | YesCT: Move token seed in...

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.