diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..7d2c7033 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,22 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + groups: + github-actions: + patterns: + - "*" + + - package-ecosystem: npm + directory: / + schedule: + interval: monthly + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-major"] + groups: + npm-dependencies: + patterns: + - "*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c1182bd..004c35ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,9 @@ on: - pull_request - push +permissions: + contents: read + jobs: test: runs-on: ubuntu-latest @@ -29,6 +32,15 @@ jobs: - Node.js 14.x - Node.js 15.x - Node.js 16.x + - Node.js 17.x + - Node.js 18.x + - Node.js 19.x + - Node.js 20.x + - Node.js 21.x + - Node.js 22.x + - Node.js 23.x + - Node.js 24.x + - Node.js 25.x include: - name: Node.js 0.8 @@ -38,23 +50,23 @@ jobs: - name: Node.js 0.10 node-version: "0.10" - npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 + npm-i: mocha@2.5.3 nyc@10.3.2 supertest@2.0.0 - name: Node.js 0.12 node-version: "0.12" - npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 + npm-i: mocha@2.5.3 nyc@10.3.2 supertest@2.0.0 - name: io.js 1.x node-version: "1.8" - npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 + npm-i: mocha@2.5.3 nyc@10.3.2 supertest@2.0.0 - name: io.js 2.x node-version: "2.5" - npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 + npm-i: mocha@2.5.3 nyc@10.3.2 supertest@2.0.0 - name: io.js 3.x node-version: "3.3" - npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 + npm-i: mocha@2.5.3 nyc@10.3.2 supertest@2.0.0 - name: Node.js 4.x node-version: "4.9" @@ -66,55 +78,100 @@ jobs: - name: Node.js 6.x node-version: "6.17" - npm-i: mocha@6.2.2 nyc@14.1.1 + npm-i: mocha@6.2.2 nyc@14.1.1 supertest@6.1.6 - name: Node.js 7.x node-version: "7.10" - npm-i: mocha@6.2.2 nyc@14.1.1 + npm-i: mocha@6.2.2 nyc@14.1.1 supertest@6.1.6 - name: Node.js 8.x node-version: "8.17" - npm-i: mocha@7.2.0 + npm-i: mocha@7.2.0 nyc@14.1.1 supertest@6.1.6 - name: Node.js 9.x node-version: "9.11" - npm-i: mocha@7.2.0 + npm-i: mocha@7.2.0 nyc@14.1.1 supertest@6.1.6 - name: Node.js 10.x node-version: "10.24" + npm-i: mocha@8.4.0 supertest@6.1.6 - name: Node.js 11.x node-version: "11.15" + npm-i: mocha@8.4.0 supertest@6.1.6 - name: Node.js 12.x node-version: "12.22" + npm-i: mocha@9.2.2 supertest@6.1.6 - name: Node.js 13.x node-version: "13.14" + npm-i: mocha@9.2.2 supertest@6.1.6 - name: Node.js 14.x - node-version: "14.17" + node-version: "14.21" - name: Node.js 15.x - node-version: "15.14" + node-version: "15.14" + npm-i: supertest@6.1.6 - name: Node.js 16.x - node-version: "16.2" + node-version: "16.20" + + - name: Node.js 17.x + node-version: "17.9" + + - name: Node.js 18.x + node-version: "18.19" + + - name: Node.js 19.x + node-version: "19.9" + + - name: Node.js 20.x + node-version: "20" + + - name: Node.js 21.x + node-version: "21" + + - name: Node.js 22.x + node-version: "22" + + - name: Node.js 23.x + node-version: "23" + + - name: Node.js 24.x + node-version: "24" + + - name: Node.js 25.x + node-version: "25" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Install Node.js ${{ matrix.node-version }} shell: bash -eo pipefail -l {0} run: | nvm install --default ${{ matrix.node-version }} - if [[ "${{ matrix.node-version }}" == 0.* ]]; then + if [[ "${{ matrix.node-version }}" == 0.* && "$(cut -d. -f2 <<< "${{ matrix.node-version }}")" -lt 10 ]]; then + nvm install --alias=npm 0.10 + nvm use ${{ matrix.node-version }} + if [[ "$(npm -v)" == 1.1.* ]]; then + nvm exec npm npm install -g npm@1.1 + ln -fs "$(which npm)" "$(dirname "$(nvm which npm)")/npm" + else + sed -i '1s;^.*$;'"$(printf '#!%q' "$(nvm which npm)")"';' "$(readlink -f "$(which npm)")" + fi npm config set strict-ssl false fi dirname "$(nvm which ${{ matrix.node-version }})" >> "$GITHUB_PATH" - name: Configure npm - run: npm config set shrinkwrap false + run: | + if [[ "$(npm config get package-lock)" == "true" ]]; then + npm config set package-lock false + else + npm config set shrinkwrap false + fi - name: Remove npm module(s) ${{ matrix.npm-rm }} run: npm rm --silent --save-dev ${{ matrix.npm-rm }} @@ -128,8 +185,8 @@ jobs: shell: bash run: | # eslint for linting - # - remove on Node.js < 10 - if [[ "$(cut -d. -f1 <<< "${{ matrix.node-version }}")" -lt 10 ]]; then + # - remove on Node.js < 12 + if [[ "$(cut -d. -f1 <<< "${{ matrix.node-version }}")" -lt 12 ]]; then node -pe 'Object.keys(require("./package").devDependencies).join("\n")' | \ grep -E '^eslint(-|$)' | \ sort -r | \ @@ -146,13 +203,14 @@ jobs: echo "node@$(node -v)" echo "npm@$(npm -v)" npm -s ls ||: - (npm -s ls --depth=0 ||:) | awk -F'[ @]' 'NR>1 && $2 { print "::set-output name=" $2 "::" $3 }' + (npm -s ls --depth=0 ||:) | awk -F'[ @]' 'NR>1 && $2 { print $2 "=" $3 }' >> "$GITHUB_OUTPUT" - name: Run tests shell: bash run: | if npm -ps ls nyc | grep -q nyc; then npm run test-ci + cp coverage/lcov.info "coverage/${{ matrix.node-version }}.lcov" else npm test fi @@ -162,19 +220,45 @@ jobs: run: npm run lint - name: Collect code coverage - uses: coverallsapp/github-action@master + if: steps.list_env.outputs.nyc != '' + run: | + if [[ -d ./coverage ]]; then + mv ./coverage "./${{ matrix.node-version }}" + mkdir ./coverage + mv "./${{ matrix.node-version }}" "./coverage/${{ matrix.node-version }}" + fi + + - name: Upload code coverage + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: steps.list_env.outputs.nyc != '' with: - github-token: ${{ secrets.GITHUB_TOKEN }} - flag-name: run-${{ matrix.test_number }} - parallel: true + name: coverage-${{ matrix.node-version }} + path: "./coverage/${{ matrix.node-version }}" + retention-days: 1 coverage: + permissions: + checks: write # for coverallsapp/github-action to create new checks + contents: read # for actions/checkout to fetch code needs: test runs-on: ubuntu-latest steps: - - name: Upload code coverage - uses: coverallsapp/github-action@master + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Install lcov + shell: bash + run: sudo apt-get -y install lcov + + - name: Collect coverage reports + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + path: ./coverage + + - name: Merge coverage reports + shell: bash + run: find ./coverage -name lcov.info -exec printf '-a %q\n' {} \; | xargs lcov -o ./coverage/lcov.info + + - name: Upload coverage report + uses: coverallsapp/github-action@5cbfd81b66ca5d10c19b062c04de0199c215fb6e # master with: - github-token: ${{ secrets.github_token }} - parallel-finished: true + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..139ec706 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,66 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: ["master"] + pull_request: + # The branches below must be a subset of the branches above + branches: ["master"] + schedule: + - cron: "0 0 * * 1" + +permissions: + contents: read + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + steps: + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 + with: + languages: javascript + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + # - name: Autobuild + # uses: github/codeql-action/autobuild@3ab4101902695724f9365a384f86c1074d94e18c # v3.24.7 + + # âšī¸ Command-line programs to run using the OS shell. + # đ See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 + with: + category: "/language:javascript" \ No newline at end of file diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 00000000..2bb15b23 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,73 @@ +# This workflow uses actions that are not certified by GitHub. They are provided +# by a third-party and are governed by separate terms of service, privacy +# policy, and support documentation. + +name: Scorecard supply-chain security + +on: + # For Branch-Protection check. Only the default branch is supported. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection + branch_protection_rule: + # To guarantee Maintained check is occasionally updated. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained + schedule: + - cron: '16 21 * * 1' + push: + branches: [ "master" ] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Needed to publish results and get a badge (see publish_results below). + id-token: write + # Uncomment the permissions below if installing in a private repository. + # contents: read + # actions: read + + steps: + - name: "Checkout code" + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 + with: + results_file: results.sarif + results_format: sarif + # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: + # - you want to enable the Branch-Protection check on a *public* repository, or + # - you are installing Scorecard on a *private* repository + # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. + # repo_token: ${{ secrets.SCORECARD_TOKEN }} + + # Public repositories: + # - Publish results to OpenSSF REST API for easy access by consumers + # - Allows the repository to include the Scorecard badge. + # - See https://github.com/ossf/scorecard-action#publishing-results. + # For private repositories: + # - `publish_results` will always be set to `false`, regardless + # of the value entered here. + publish_results: true + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: "Upload artifact" + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard. + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 + with: + sarif_file: results.sarif \ No newline at end of file diff --git a/.gitignore b/.gitignore index 207febba..1b6fef23 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ coverage node_modules npm-debug.log package-lock.json +/test/fixtures/server.crt +/test/fixtures/server.key diff --git a/HISTORY.md b/HISTORY.md index 774607dc..251d5fa7 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,56 @@ +unreleased +========== + + * Enforce RFC 6265 section 5.1.4 path matching for the session cookie path + * Fix `secure: 'auto'` and `sameSite: 'auto'` not being resolved when `cookie` is a function + +1.19.0 / 2026-01-22 +========== + + * Add dynamic cookie options support + * Add sameSite 'auto' support for automatic SameSite attribute configuration + * deps: use tilde notation for dependencies + +1.18.2 / 2025-07-17 +========== + * deps: mocha@10.8.2 + * deps: on-headers@~1.1.0 + - Fix [CVE-2025-7339](https://www.cve.org/CVERecord?id=CVE-2025-7339) ([GHSA-76c9-3jph-rj3q](https://github.com/expressjs/on-headers/security/advisories/GHSA-76c9-3jph-rj3q)) + +1.18.1 / 2024-10-08 +========== + + * deps: cookie@0.7.2 + - Fix object assignment of `hasOwnProperty` + * deps: cookie@0.7.1 + - Allow leading dot for domain + - Although not permitted in the spec, some users expect this to work and user agents ignore the leading dot according to spec + - Add fast path for `serialize` without options, use `obj.hasOwnProperty` when parsing + * deps: cookie@0.7.0 + - perf: parse cookies ~10% faster + - fix: narrow the validation of cookies to match RFC6265 + - fix: add `main` to `package.json` for rspack + +1.18.0 / 2024-01-28 +=================== + + * Add debug log for pathname mismatch + * Add `partitioned` to `cookie` options + * Add `priority` to `cookie` options + * Fix handling errors from setting cookie + * Support any type in `secret` that `crypto.createHmac` supports + * deps: cookie@0.6.0 + - Fix `expires` option to reject invalid dates + - perf: improve default decode speed + - perf: remove slow string split in parse + * deps: cookie-signature@1.0.7 + +1.17.3 / 2022-05-11 +=================== + + * Fix resaving already-saved new session at end of request + * deps: cookie@0.4.2 + 1.17.2 / 2021-05-19 =================== @@ -132,7 +185,7 @@ - Improve error message when `expires` is not a `Date` - perf: enable strict mode - perf: use for loop in parse - - perf: use string concatination for serialization + - perf: use string concatenation for serialization * deps: parseurl@~1.3.1 - perf: enable strict mode * deps: uid-safe@~2.1.1 diff --git a/README.md b/README.md index 01477c39..162ea14d 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,26 @@ For a list of stores, see [compatible session stores](#compatible-session-stores Settings object for the session ID cookie. The default value is `{ path: '/', httpOnly: true, secure: false, maxAge: null }`. +In addition to providing a static object, you can also pass a callback function to dynamically generate the cookie options for each request. The callback receives the `req` object as its argument and should return an object containing the cookie settings. + +```js +var app = express() +app.use(session({ + secret: 'keyboard cat', + resave: false, + saveUninitialized: true, + cookie: function(req) { + var match = req.url.match(/^\/([^/]+)/); + return { + path: match ? '/' + match[1] : '/', + httpOnly: true, + secure: req.secure || false, + maxAge: 60000 + } + } +})) +``` + The following are options that can be set in this object. ##### cookie.domain @@ -89,20 +109,54 @@ no maximum age is set. **Note** If both `expires` and `maxAge` are set in the options, then the last one defined in the object is what is used. +##### cookie.partitioned + +Specifies the `boolean` value for the [`Partitioned` `Set-Cookie`](rfc-cutler-httpbis-partitioned-cookies) +attribute. When truthy, the `Partitioned` attribute is set, otherwise it is not. +By default, the `Partitioned` attribute is not set. + +**Note** This is an attribute that has not yet been fully standardized, and may +change in the future. This also means many clients may ignore this attribute until +they understand it. + +More information about can be found in [the proposal](https://github.com/privacycg/CHIPS). + ##### cookie.path Specifies the value for the `Path` `Set-Cookie`. By default, this is set to `'/'`, which is the root path of the domain. +Since 1.19.1, path matching follows [RFC 6265 section 5.1.4][rfc-6265-5.1.4]. This means +the session middleware will only activate when the request path is an exact match or falls +under a segment boundary of the cookie path. For example, a cookie path of `/admin` will +match `/admin` and `/admin/users` but will **not** match `/administrator`. Prior versions +used a simple prefix check that did not enforce segment boundaries. + +##### cookie.priority + +Specifies the `string` to be the value for the [`Priority` `Set-Cookie` attribute][rfc-west-cookie-priority-00-4.1]. + + - `'low'` will set the `Priority` attribute to `Low`. + - `'medium'` will set the `Priority` attribute to `Medium`, the default priority when not set. + - `'high'` will set the `Priority` attribute to `High`. + +More information about the different priority levels can be found in +[the specification][rfc-west-cookie-priority-00-4.1]. + +**Note** This is an attribute that has not yet been fully standardized, and may change in the future. +This also means many clients may ignore this attribute until they understand it. + ##### cookie.sameSite Specifies the `boolean` or `string` to be the value for the `SameSite` `Set-Cookie` attribute. +By default, this is `false`. - `true` will set the `SameSite` attribute to `Strict` for strict same site enforcement. - `false` will not set the `SameSite` attribute. - `'lax'` will set the `SameSite` attribute to `Lax` for lax same site enforcement. - `'none'` will set the `SameSite` attribute to `None` for an explicit cross-site cookie. - `'strict'` will set the `SameSite` attribute to `Strict` for strict same site enforcement. + - `'auto'` will set the `SameSite` attribute to `None` for secure connections and `Lax` for non-secure connections. More information about the different enforcement levels can be found in [the specification][rfc-6265bis-03-4.1.2.7]. @@ -114,6 +168,14 @@ the future. This also means many clients may ignore this attribute until they un that requires that the `Secure` attribute be set to `true` when the `SameSite` attribute has been set to `'none'`. Some web browsers or other clients may be adopting this specification. +The `cookie.sameSite` option can also be set to the special value `'auto'` to have +this setting automatically match the determined security of the connection. When the connection +is secure (HTTPS), the `SameSite` attribute will be set to `None` to enable cross-site usage. +When the connection is not secure (HTTP), the `SameSite` attribute will be set to `Lax` for +better security while maintaining functionality. This is useful when the Express `"trust proxy"` +setting is properly setup to simplify development vs production configuration, particularly +for SAML authentication scenarios. + ##### cookie.secure Specifies the `boolean` value for the `Secure` `Set-Cookie` attribute. When truthy, @@ -242,7 +304,7 @@ With this enabled, the session identifier cookie will expire in [`maxAge`](#cookiemaxage) since the last response was sent instead of in [`maxAge`](#cookiemaxage) since the session was last modified by the server. -This is typically used in conjuction with short, non-session-length +This is typically used in conjunction with short, non-session-length [`maxAge`](#cookiemaxage) values to provide a quick timeout of the session data with reduced potential of it occurring during on going server interactions. @@ -273,12 +335,13 @@ it to be saved. *This has been fixed in PassportJS 0.3.0* **Required option** -This is the secret used to sign the session ID cookie. This can be either a string -for a single secret, or an array of multiple secrets. If an array of secrets is -provided, only the first element will be used to sign the session ID cookie, while -all the elements will be considered when verifying the signature in requests. The -secret itself should be not easily parsed by a human and would best be a random set -of characters. A best practice may include: +This is the secret used to sign the session ID cookie. The secret can be any type +of value that is supported by Node.js `crypto.createHmac` (like a string or a +`Buffer`). This can be either a single secret, or an array of multiple secrets. If +an array of secrets is provided, only the first element will be used to sign the +session ID cookie, while all the elements will be considered when verifying the +signature in requests. The secret itself should be not easily parsed by a human and +would best be a random set of characters. A best practice may include: - The use of environment variables to store the secret, ensuring the secret itself does not exist in your repository. @@ -293,6 +356,9 @@ the secret without invalidating sessions, provide an array of secrets, with the secret as first element of the array, and including previous secrets as the later elements. +**Note** HMAC-256 is used to sign the session ID. For this reason, the secret should +contain at least 32 bytes of entropy. + ##### store The session store instance, defaults to a new `MemoryStore` instance. @@ -555,6 +621,11 @@ and other multi-core embedded devices). [connect-cloudant-store-url]: https://www.npmjs.com/package/connect-cloudant-store [connect-cloudant-store-image]: https://badgen.net/github/stars/adriantanasa/connect-cloudant-store?label=%E2%98%85 +[![â ][connect-cosmosdb-image] connect-cosmosdb][connect-cosmosdb-url] An Azure [Cosmos DB](https://azure.microsoft.com/en-us/products/cosmos-db/)-based session store. + +[connect-cosmosdb-url]: https://www.npmjs.com/package/connect-cosmosdb +[connect-cosmosdb-image]: https://badgen.net/github/stars/thekillingspree/connect-cosmosdb?label=%E2%98%85 + [![â ][connect-couchbase-image] connect-couchbase][connect-couchbase-url] A [couchbase](http://www.couchbase.com/)-based session store. [connect-couchbase-url]: https://www.npmjs.com/package/connect-couchbase @@ -595,6 +666,11 @@ and other multi-core embedded devices). [connect-loki-url]: https://www.npmjs.com/package/connect-loki [connect-loki-image]: https://badgen.net/github/stars/Requarks/connect-loki?label=%E2%98%85 +[![â ][connect-lowdb-image] connect-lowdb][connect-lowdb-url] A lowdb-based session store. + +[connect-lowdb-url]: https://www.npmjs.com/package/connect-lowdb +[connect-lowdb-image]: https://badgen.net/github/stars/travishorn/connect-lowdb?label=%E2%98%85 + [![â ][connect-memcached-image] connect-memcached][connect-memcached-url] A memcached-based session store. [connect-memcached-url]: https://www.npmjs.com/package/connect-memcached @@ -631,6 +707,16 @@ and other multi-core embedded devices). [connect-mssql-v2-url]: https://www.npmjs.com/package/connect-mssql-v2 [connect-mssql-v2-image]: https://badgen.net/github/stars/jluboff/connect-mssql-v2?label=%E2%98%85 +[![â ][connect-neo4j-image] connect-neo4j][connect-neo4j-url] A [Neo4j](https://neo4j.com)-based session store. + +[connect-neo4j-url]: https://www.npmjs.com/package/connect-neo4j +[connect-neo4j-image]: https://badgen.net/github/stars/MaxAndersson/connect-neo4j?label=%E2%98%85 + +[![â ][connect-ottoman-image] connect-ottoman][connect-ottoman-url] A [couchbase ottoman](http://www.couchbase.com/)-based session store. + +[connect-ottoman-url]: https://www.npmjs.com/package/connect-ottoman +[connect-ottoman-image]: https://badgen.net/github/stars/noiissyboy/connect-ottoman?label=%E2%98%85 + [![â ][connect-pg-simple-image] connect-pg-simple][connect-pg-simple-url] A PostgreSQL-based session store. [connect-pg-simple-url]: https://www.npmjs.com/package/connect-pg-simple @@ -653,7 +739,7 @@ and other multi-core embedded devices). [connect-session-knex-image]: https://badgen.net/github/stars/llambda/connect-session-knex?label=%E2%98%85 [![â ][connect-session-sequelize-image] connect-session-sequelize][connect-session-sequelize-url] A session store using -[Sequelize.js](http://sequelizejs.com/), which is a Node.js / io.js ORM for PostgreSQL, MySQL, SQLite and MSSQL. +[Sequelize.js](https://sequelize.org/), which is a Node.js / io.js ORM for PostgreSQL, MySQL, SQLite and MSSQL. [connect-session-sequelize-url]: https://www.npmjs.com/package/connect-session-sequelize [connect-session-sequelize-image]: https://badgen.net/github/stars/mweibel/connect-session-sequelize?label=%E2%98%85 @@ -717,7 +803,7 @@ a [variety of storage types](https://www.npmjs.com/package/cache-manager#store-e [express-session-level-url]: https://www.npmjs.com/package/express-session-level [express-session-level-image]: https://badgen.net/github/stars/tgohn/express-session-level?label=%E2%98%85 -[![â ][express-session-rsdb-image] express-session-rsdb][express-session-rsdb-url] Session store based on Rocket-Store: A very simple, super fast and yet powerfull, flat file database. +[![â ][express-session-rsdb-image] express-session-rsdb][express-session-rsdb-url] Session store based on Rocket-Store: A very simple, super fast and yet powerful, flat file database. [express-session-rsdb-url]: https://www.npmjs.com/package/express-session-rsdb [express-session-rsdb-image]: https://badgen.net/github/stars/paragi/express-session-rsdb?label=%E2%98%85 @@ -783,7 +869,7 @@ based session store. Supports all backends supported by Fortune (MongoDB, Redis, [restsession-url]: https://www.npmjs.com/package/restsession [restsession-image]: https://badgen.net/github/stars/jankal/restsession?label=%E2%98%85 -[![â ][sequelstore-connect-image] sequelstore-connect][sequelstore-connect-url] A session store using [Sequelize.js](http://sequelizejs.com/). +[![â ][sequelstore-connect-image] sequelstore-connect][sequelstore-connect-url] A session store using [Sequelize.js](https://sequelize.org/). [sequelstore-connect-url]: https://www.npmjs.com/package/sequelstore-connect [sequelstore-connect-image]: https://badgen.net/github/stars/MattMcFarland/sequelstore-connect?label=%E2%98%85 @@ -798,11 +884,6 @@ based session store. Supports all backends supported by Fortune (MongoDB, Redis, [session-pouchdb-store-url]: https://www.npmjs.com/package/session-pouchdb-store [session-pouchdb-store-image]: https://badgen.net/github/stars/solzimer/session-pouchdb-store?label=%E2%98%85 -[![â ][session-rethinkdb-image] session-rethinkdb][session-rethinkdb-url] A [RethinkDB](http://rethinkdb.com/)-based session store. - -[session-rethinkdb-url]: https://www.npmjs.com/package/session-rethinkdb -[session-rethinkdb-image]: https://badgen.net/github/stars/llambda/session-rethinkdb?label=%E2%98%85 - [![â ][@databunker/session-store-image] @databunker/session-store][@databunker/session-store-url] A [Databunker](https://databunker.org/)-based encrypted session store. [@databunker/session-store-url]: https://www.npmjs.com/package/@databunker/session-store @@ -818,7 +899,9 @@ based session store. Supports all backends supported by Fortune (MongoDB, Redis, [tch-nedb-session-url]: https://www.npmjs.com/package/tch-nedb-session [tch-nedb-session-image]: https://badgen.net/github/stars/tomaschyly/NeDBSession?label=%E2%98%85 -## Example +## Examples + +### View counter A simple example using `express-session` to store page views for a user. @@ -856,6 +939,87 @@ app.get('/foo', function (req, res, next) { app.get('/bar', function (req, res, next) { res.send('you viewed this page ' + req.session.views['/bar'] + ' times') }) + +app.listen(3000) +``` + +### User login + +A simple example using `express-session` to keep a user log in session. + +```js +var escapeHtml = require('escape-html') +var express = require('express') +var session = require('express-session') + +var app = express() + +app.use(session({ + secret: 'keyboard cat', + resave: false, + saveUninitialized: true +})) + +// middleware to test if authenticated +function isAuthenticated (req, res, next) { + if (req.session.user) next() + else next('route') +} + +app.get('/', isAuthenticated, function (req, res) { + // this is only called when there is an authentication user due to isAuthenticated + res.send('hello, ' + escapeHtml(req.session.user) + '!' + + ' Logout') +}) + +app.get('/', function (req, res) { + res.send('
') +}) + +app.post('/login', express.urlencoded({ extended: false }), function (req, res) { + // login logic to validate req.body.user and req.body.pass + // would be implemented here. for this example any combo works + + // regenerate the session, which is good practice to help + // guard against forms of session fixation + req.session.regenerate(function (err) { + if (err) next(err) + + // store user information in session, typically a user id + req.session.user = req.body.user + + // save the session before redirection to ensure page + // load does not happen before session is saved + req.session.save(function (err) { + if (err) return next(err) + res.redirect('/') + }) + }) +}) + +app.get('/logout', function (req, res, next) { + // logout logic + + // clear the user from the session object and save. + // this will ensure that re-using the old session id + // does not have a logged in user + req.session.user = null + req.session.save(function (err) { + if (err) next(err) + + // regenerate the session, which is good practice to help + // guard against forms of session fixation + req.session.regenerate(function (err) { + if (err) next(err) + res.redirect('/') + }) + }) +}) + +app.listen(3000) ``` ## Debugging @@ -880,7 +1044,10 @@ On Windows, use the corresponding command; [MIT](LICENSE) +[rfc-6265-5.1.4]: https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4 [rfc-6265bis-03-4.1.2.7]: https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7 +[rfc-cutler-httpbis-partitioned-cookies]: https://tools.ietf.org/html/draft-cutler-httpbis-partitioned-cookies/ +[rfc-west-cookie-priority-00-4.1]: https://tools.ietf.org/html/draft-west-cookie-priority-00#section-4.1 [ci-image]: https://badgen.net/github/checks/expressjs/session/master?label=ci [ci-url]: https://github.com/expressjs/session/actions?query=workflow%3Aci [coveralls-image]: https://badgen.net/coveralls/c/github/expressjs/session/master diff --git a/index.js b/index.js index d7efeab9..3e23e901 100644 --- a/index.js +++ b/index.js @@ -70,7 +70,7 @@ var defer = typeof setImmediate === 'function' * Setup session store with the given `options`. * * @param {Object} [options] - * @param {Object} [options.cookie] Options for cookie + * @param {Object|Function} [options.cookie] Options for cookie * @param {Function} [options.genid] * @param {String} [options.name=connect.sid] Session ID cookie name * @param {Boolean} [options.proxy] @@ -158,10 +158,17 @@ function session(options) { store.generate = function(req){ req.sessionID = generateId(req); req.session = new Session(req); - req.session.cookie = new Cookie(cookieOptions); + var resolvedCookieOptions = typeof cookieOptions === 'function' ? cookieOptions(req) : cookieOptions; + req.session.cookie = new Cookie(resolvedCookieOptions); - if (cookieOptions.secure === 'auto') { - req.session.cookie.secure = issecure(req, trustProxy); + var isSecure = issecure(req, trustProxy); + + if (resolvedCookieOptions.secure === 'auto') { + req.session.cookie.secure = isSecure; + } + + if (resolvedCookieOptions.sameSite === 'auto') { + req.session.cookie.sameSite = isSecure ? 'none' : 'lax'; } }; @@ -193,7 +200,15 @@ function session(options) { // pathname mismatch var originalPath = parseUrl.original(req).pathname || '/' - if (originalPath.indexOf(cookieOptions.path || '/') !== 0) return next(); + var resolvedCookieOptions = typeof cookieOptions === 'function' ? cookieOptions(req) : cookieOptions + var cfgPath = resolvedCookieOptions.path || '/' + + if (!rfcPathMatch(originalPath, cfgPath)) { + debug('pathname mismatch') + next() + return + } + // ensure a secret is available or bail if (!secret && !req.secret) { @@ -240,7 +255,11 @@ function session(options) { } // set cookie - setcookie(res, name, req.sessionID, secrets[0], req.session.cookie.data); + try { + setcookie(res, name, req.sessionID, secrets[0], req.session.cookie.data) + } catch (err) { + defer(next, err) + } }); // proxy end() to commit the session @@ -444,7 +463,7 @@ function session(options) { return false; } - return !saveUninitializedSession && cookieId !== req.sessionID + return !saveUninitializedSession && !savedHash && cookieId !== req.sessionID ? isModified(req.session) : !isSaved(req.session) } @@ -508,6 +527,42 @@ function session(options) { }; }; +/** + * Check if the cookiePath matches the requestPath following the + * rules in RFC 6265 section 5.1.4. + * + * @param {String} requestPath + * @param {String} cookiePath + * @return {Boolean} + * @private + */ + +function rfcPathMatch(requestPath, cookiePath) { + // Normalize inputs (Node 0.8-safe) + requestPath = (typeof requestPath === 'string' && requestPath.length) ? requestPath : '/'; + cookiePath = (typeof cookiePath === 'string' && cookiePath.length) ? cookiePath : '/'; + + // Root cookie matches everything + if (cookiePath === '/') return true; + + // Exact match + if (requestPath === cookiePath) return true; + + // Prefix match + if (requestPath.indexOf(cookiePath) === 0) { + // If cookiePath ends with '/', any longer requestPath is OK + if (cookiePath.charAt(cookiePath.length - 1) === '/') return true; + + // Otherwise the next char after the prefix must be '/' + var nextChar = requestPath.length > cookiePath.length + ? requestPath.charAt(cookiePath.length) + : ''; + return nextChar === '/'; + } + + return false; +} + /** * Generate a session ID for a new session. * @@ -515,7 +570,7 @@ function session(options) { * @private */ -function generateSessionId(sess) { +function generateSessionId() { return uid(24); } @@ -538,7 +593,7 @@ function getcookie(req, name, secrets) { raw = cookies[name]; if (raw) { - if (raw.substr(0, 2) === 's:') { + if (raw.slice(0, 2) === 's:') { val = unsigncookie(raw.slice(2), secrets); if (val === false) { @@ -565,7 +620,7 @@ function getcookie(req, name, secrets) { raw = req.cookies[name]; if (raw) { - if (raw.substr(0, 2) === 's:') { + if (raw.slice(0, 2) === 's:') { val = unsigncookie(raw.slice(2), secrets); if (val) { @@ -622,7 +677,8 @@ function hash(sess) { function issecure(req, trustProxy) { // socket is https server - if (req.connection && req.connection.encrypted) { + var socket = req.socket || req.connection; + if (socket && socket.encrypted) { return true; } @@ -640,7 +696,7 @@ function issecure(req, trustProxy) { var header = req.headers['x-forwarded-proto'] || ''; var index = header.indexOf(','); var proto = index !== -1 - ? header.substr(0, index).toLowerCase().trim() + ? header.slice(0, index).toLowerCase().trim() : header.toLowerCase().trim() return proto === 'https'; diff --git a/package.json b/package.json index d085f292..36387796 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "express-session", - "version": "1.17.2", + "version": "1.19.0", "description": "Simple session middleware for Express", "author": "TJ Holowaychuk