diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 01c82f81960..b3ddd6b23ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,196 +1,168 @@ name: ci on: -- pull_request -- push + push: + branches: + - master + - develop + - '4.x' + - '5.x' + paths-ignore: + - '*.md' + pull_request: + paths-ignore: + - '*.md' + +# Cancel in progress workflows +# in the scenario where we already had a run going for that PR/branch/tag but then triggered a new run +concurrency: + group: "${{ github.workflow }} ✨ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" + cancel-in-progress: true jobs: - test: + lint: + name: Lint runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup Node.js {{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: 'lts/*' + persist-credentials: false + + - name: Install dependencies + run: npm install --ignore-scripts --only=dev + + - name: Run lint + run: npm run lint + + test: + name: Run tests strategy: fail-fast: false matrix: - name: - - Node.js 0.10 - - Node.js 0.12 - - io.js 1.x - - io.js 2.x - - io.js 3.x - - Node.js 4.x - - Node.js 5.x - - Node.js 6.x - - Node.js 7.x - - Node.js 8.x - - Node.js 9.x - - Node.js 10.x - - Node.js 11.x - - Node.js 12.x - - Node.js 13.x - - 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 - + os: [ubuntu-latest, windows-latest] + node-version: + - "0.10" + - "0.12" + - "4" + - "5" + - "6" + - "7" + - "8" + - "9" + - "10" + - "11" + - "12" + - "13" + - "14" + - "15" + - "16" + - "17" + - "18" + - "19" + - "20" + - "21" + - "22" + # Use supported versions of our testing tools under older versions of Node + # Install npm in some specific cases where we need to include: - - name: Node.js 0.10 - node-version: "0.10" - npm-i: mocha@3.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 - - - name: io.js 1.x - node-version: "1.8" - npm-i: mocha@3.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 - - - name: io.js 3.x - node-version: "3.3" - npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 - - - name: Node.js 4.x - node-version: "4.9" - npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 - - - name: Node.js 5.x - node-version: "5.12" - npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 - - - name: Node.js 6.x - node-version: "6.17" - npm-i: mocha@6.2.2 nyc@14.1.1 supertest@3.4.2 + - node-version: "0.10" + npm-i: "mocha@3.5.3 nyc@10.3.2 supertest@2.0.0" + # Npm isn't being installed on windows w/ setup-node for + # 0.10 and 0.12, which will end up choking when npm uses es6 + npm-version: "npm@2.15.1" - - name: Node.js 7.x - node-version: "7.10" - npm-i: mocha@6.2.2 nyc@14.1.1 supertest@6.1.6 + - node-version: "0.12" + npm-i: "mocha@3.5.3 nyc@10.3.2 supertest@2.0.0" + npm-version: "npm@2.15.11" - - name: Node.js 8.x - node-version: "8.17" - npm-i: mocha@7.2.0 nyc@14.1.1 + - node-version: "4" + npm-i: "mocha@5.2.0 nyc@11.9.0 supertest@3.4.2" - - name: Node.js 9.x - node-version: "9.11" - npm-i: mocha@7.2.0 nyc@14.1.1 + - node-version: "5" + npm-i: "mocha@5.2.0 nyc@11.9.0 supertest@3.4.2" + # fixes https://github.com/npm/cli/issues/681 + npm-version: "npm@3.10.10" - - name: Node.js 10.x - node-version: "10.24" - npm-i: mocha@8.4.0 + - node-version: "6" + npm-i: "mocha@6.2.2 nyc@14.1.1 supertest@3.4.2" - - name: Node.js 11.x - node-version: "11.15" - npm-i: mocha@8.4.0 + - node-version: "7" + npm-i: "mocha@6.2.2 nyc@14.1.1 supertest@6.1.6" - - name: Node.js 12.x - node-version: "12.22" - npm-i: mocha@9.2.2 + - node-version: "8" + npm-i: "mocha@7.2.0 nyc@14.1.1" - - name: Node.js 13.x - node-version: "13.14" - npm-i: mocha@9.2.2 + - node-version: "9" + npm-i: "mocha@7.2.0 nyc@14.1.1" - - name: Node.js 14.x - node-version: "14.20" + - node-version: "10" + npm-i: "mocha@8.4.0" - - name: Node.js 15.x - node-version: "15.14" + - node-version: "11" + npm-i: "mocha@8.4.0" - - name: Node.js 16.x - node-version: "16.20" + - node-version: "12" + npm-i: "mocha@9.2.2" - - 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.11" - - - name: Node.js 21.x - node-version: "21.6" + - node-version: "13" + npm-i: "mocha@9.2.2" + runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 - - - name: Install Node.js ${{ matrix.node-version }} - shell: bash -eo pipefail -l {0} - run: | - nvm install --default ${{ matrix.node-version }} - dirname "$(nvm which ${{ matrix.node-version }})" >> "$GITHUB_PATH" - - - name: Configure npm - run: | - npm config set loglevel error - if [[ "$(npm config get package-lock)" == "true" ]]; then - npm config set package-lock false - else - npm config set shrinkwrap false - fi - - - name: Install npm module(s) ${{ matrix.npm-i }} - run: npm install --save-dev ${{ matrix.npm-i }} - if: matrix.npm-i != '' - - - name: Remove non-test dependencies - run: npm rm --silent --save-dev connect-redis - - - name: Setup Node.js version-specific dependencies - shell: bash - run: | - # eslint for linting - # - 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 | \ - xargs -n1 npm rm --silent --save-dev - fi - - - name: Install Node.js dependencies - run: npm install - - - name: List environment - id: list_env - shell: bash - run: | - echo "node@$(node -v)" - echo "npm@$(npm -v)" - npm -s ls ||: - (npm -s ls --depth=0 ||:) | awk -F'[ @]' 'NR>1 && $2 { print $2 "=" $3 }' >> "$GITHUB_OUTPUT" - - - name: Run tests - shell: bash - run: | - npm run test-ci - cp coverage/lcov.info "coverage/${{ matrix.name }}.lcov" - - - name: Lint code - if: steps.list_env.outputs.eslint != '' - run: npm run lint - - - name: Collect code coverage - run: | - mv ./coverage "./${{ matrix.name }}" - mkdir ./coverage - mv "./${{ matrix.name }}" "./coverage/${{ matrix.name }}" - - - name: Upload code coverage - uses: actions/upload-artifact@v3 - with: - name: coverage - path: ./coverage - retention-days: 1 + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Npm version fixes + if: ${{matrix.npm-version != ''}} + run: npm install -g ${{ matrix.npm-version }} + + - name: Configure npm loglevel + run: | + npm config set loglevel error + shell: bash + + - name: Install dependencies + run: npm install + + - name: Install Node version specific dev deps + if: ${{ matrix.npm-i != '' }} + run: npm install --save-dev ${{ matrix.npm-i }} + + - name: Remove non-test dependencies + run: npm rm --silent --save-dev connect-redis + + - name: Output Node and NPM versions + run: | + echo "Node.js version: $(node -v)" + echo "NPM version: $(npm -v)" + + - name: Run tests + shell: bash + run: | + npm run test-ci + cp coverage/lcov.info "coverage/${{ matrix.node-version }}.lcov" + + - name: Collect code coverage + run: | + mv ./coverage "./${{ matrix.node-version }}" + mkdir ./coverage + mv "./${{ matrix.node-version }}" "./coverage/${{ matrix.node-version }}" + + - name: Upload code coverage + uses: actions/upload-artifact@v3 + with: + name: coverage + path: ./coverage + retention-days: 1 coverage: needs: test diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000000..db4e01aff56 --- /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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@3ab4101902695724f9365a384f86c1074d94e18c # v3.24.7 + 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@3ab4101902695724f9365a384f86c1074d94e18c # v3.24.7 + with: + category: "/language:javascript" diff --git a/.github/workflows/iojs.yml b/.github/workflows/iojs.yml new file mode 100644 index 00000000000..c1268abd689 --- /dev/null +++ b/.github/workflows/iojs.yml @@ -0,0 +1,69 @@ +name: iojs-ci + +on: + push: + branches: + - master + - '4.x' + paths-ignore: + - '*.md' + pull_request: + paths-ignore: + - '*.md' + +concurrency: + group: "${{ github.workflow }} ✨ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" + cancel-in-progress: true + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: ["1.8", "2.5", "3.3"] + include: + - node-version: "1.8" + npm-i: "mocha@3.5.3 nyc@10.3.2 supertest@2.0.0" + - node-version: "2.5" + npm-i: "mocha@3.5.3 nyc@10.3.2 supertest@2.0.0" + - node-version: "3.3" + npm-i: "mocha@3.5.3 nyc@10.3.2 supertest@2.0.0" + + steps: + - uses: actions/checkout@v4 + + - name: Install iojs ${{ matrix.node-version }} + shell: bash -eo pipefail -l {0} + run: | + nvm install --default ${{ matrix.node-version }} + dirname "$(nvm which ${{ matrix.node-version }})" >> "$GITHUB_PATH" + + - name: Configure npm + run: | + npm config set loglevel error + npm config set shrinkwrap false + + - name: Install npm module(s) ${{ matrix.npm-i }} + run: npm install --save-dev ${{ matrix.npm-i }} + if: matrix.npm-i != '' + + - name: Remove non-test dependencies + run: npm rm --silent --save-dev connect-redis + + - name: Install Node.js dependencies + run: npm install + + - name: List environment + id: list_env + shell: bash + run: | + echo "node@$(node -v)" + echo "npm@$(npm -v)" + npm -s ls ||: + (npm -s ls --depth=0 ||:) | awk -F'[ @]' 'NR>1 && $2 { print $2 "=" $3 }' >> "$GITHUB_OUTPUT" + + - name: Run tests + shell: bash + run: npm run test + diff --git a/.gitignore b/.gitignore index 3a673d9cc09..1bd5c02b28b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # npm node_modules package-lock.json +npm-shrinkwrap.json *.log *.gz diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000000..43c97e719a5 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/Code-Of-Conduct.md b/Code-Of-Conduct.md index bbb8996a659..ca4c6b31468 100644 --- a/Code-Of-Conduct.md +++ b/Code-Of-Conduct.md @@ -127,7 +127,7 @@ project community. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant, version 2.0](cc-20-doc). +This Code of Conduct is adapted from the [Contributor Covenant, version 2.0][cc-20-doc]. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). diff --git a/Contributing.md b/Contributing.md index a9ba84690cb..1654cee02f2 100644 --- a/Contributing.md +++ b/Contributing.md @@ -63,29 +63,14 @@ compromise among committers be the default resolution mechanism. Anyone can become a triager! Read more about the process of being a triager in [the triage process document](Triager-Guide.md). -[Open an issue in `expressjs/express` repo](https://github.com/expressjs/express/issues/new) -to request the triage role. State that you have read and agree to the -[Code of Conduct](Code-Of-Conduct.md) and details of the role. +Currently, any existing [organization member](https://github.com/orgs/expressjs/people) can nominate +a new triager. If you are interested in becoming a triager, our best advice is to actively participate +in the community by helping triaging issues and pull requests. As well we recommend +to engage in other community activities like attending the TC meetings, and participating in the Slack +discussions. -Here is an example issue content you can copy and paste: - -``` -Title: Request triager role for - -I have read and understood the project's Code of Conduct. -I also have read and understood the process and best practices around Express triaging. - -I request for a triager role for the following GitHub organizations: - -jshttp -pillarjs -express -``` - -Once you have opened your issue, a member of the TC will add you to the `triage` team in -the organizations requested. They will then close the issue. - -Happy triaging! +You can also reach out to any of the [organization members](https://github.com/orgs/expressjs/people) +if you have questions or need guidance. ## Becoming a Committer @@ -154,23 +139,71 @@ the project, their GitHub handle and npm username (if different). The PR will re at least 2 approvals from TC members and 2 weeks hold time to allow for comment and/or dissent. When the PR is merged, a TC member will add them to the proper GitHub/npm groups. -### Current Project Captains +### Active Projects and Captains -- `expressjs/express`: @wesleytodd +- `expressjs/badgeboard`: @wesleytodd +- `expressjs/basic-auth-connect`: N/A +- `expressjs/body-parser`: @wesleytodd, @jonchurch +- `expressjs/compression`: N/A +- `expressjs/connect-multiparty`: N/A +- `expressjs/cookie-parser`: @wesleytodd, @UlisesGascon +- `expressjs/cookie-session`: N/A +- `expressjs/cors`: @jonchurch - `expressjs/discussions`: @wesleytodd -- `expressjs/expressjs.com`: @crandmck -- `expressjs/body-parser`: @wesleytodd -- `expressjs/multer`: @LinusU -- `expressjs/cookie-parser`: @wesleytodd +- `expressjs/errorhandler`: N/A +- `expressjs/express-paginate`: N/A +- `expressjs/express`: @wesleytodd +- `expressjs/expressjs.com`: @crandmck, @jonchurch +- `expressjs/flash`: N/A - `expressjs/generator`: @wesleytodd +- `expressjs/method-override`: N/A +- `expressjs/morgan`: @jonchurch +- `expressjs/multer`: @LinusU +- `expressjs/response-time`: @blakeembrey +- `expressjs/serve-favicon`: N/A +- `expressjs/serve-index`: N/A +- `expressjs/serve-static`: N/A +- `expressjs/session`: N/A - `expressjs/statusboard`: @wesleytodd -- `pillarjs/path-to-regexp`: @blakeembrey -- `pillarjs/router`: @dougwilson, @wesleytodd -- `pillarjs/finalhandler`: @wesleytodd -- `pillarjs/request`: @wesleytodd -- `jshttp/http-errors`: @wesleytodd +- `expressjs/timeout`: N/A +- `expressjs/vhost`: N/A +- `jshttp/accepts`: @blakeembrey +- `jshttp/basic-auth`: @blakeembrey +- `jshttp/compressible`: @blakeembrey +- `jshttp/content-disposition`: @blakeembrey +- `jshttp/content-type`: @blakeembrey - `jshttp/cookie`: @wesleytodd +- `jshttp/etag`: @blakeembrey +- `jshttp/forwarded`: @blakeembrey +- `jshttp/fresh`: @blakeembrey +- `jshttp/http-assert`: @wesleytodd, @jonchurch +- `jshttp/http-errors`: @wesleytodd, @jonchurch +- `jshttp/media-typer`: @blakeembrey +- `jshttp/methods`: @blakeembrey +- `jshttp/mime-db`: @blakeembrey, @UlisesGascon +- `jshttp/mime-types`: @blakeembrey, @UlisesGascon +- `jshttp/negotiator`: @blakeembrey - `jshttp/on-finished`: @wesleytodd -- `jshttp/forwarded`: @wesleytodd +- `jshttp/on-headers`: @blakeembrey - `jshttp/proxy-addr`: @wesleytodd +- `jshttp/range-parser`: @blakeembrey +- `jshttp/statuses`: @blakeembrey +- `jshttp/type-is`: @blakeembrey +- `jshttp/vary`: @blakeembrey +- `pillarjs/cookies`: @blakeembrey +- `pillarjs/csrf`: N/A +- `pillarjs/encodeurl`: @blakeembrey +- `pillarjs/finalhandler`: @wesleytodd +- `pillarjs/hbs`: N/A +- `pillarjs/multiparty`: @blakeembrey +- `pillarjs/parseurl`: @blakeembrey +- `pillarjs/path-to-regexp`: @blakeembrey +- `pillarjs/request`: @wesleytodd +- `pillarjs/resolve-path`: @blakeembrey +- `pillarjs/router`: @blakeembrey +- `pillarjs/send`: @blakeembrey +- `pillarjs/understanding-csrf`: N/A + +### Current Initiative Captains +- Triage team [ref](https://github.com/expressjs/discussions/issues/227): @UlisesGascon diff --git a/History.md b/History.md index c4cdd94dacf..887a38f182d 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,26 @@ +4.20.0 / 2024-09-10 +========== + * deps: serve-static@0.16.0 + * Remove link renderization in html while redirecting + * deps: send@0.19.0 + * Remove link renderization in html while redirecting + * deps: body-parser@0.6.0 + * add `depth` option to customize the depth level in the parser + * IMPORTANT: The default `depth` level for parsing URL-encoded data is now `32` (previously was `Infinity`) + * Remove link renderization in html while using `res.redirect` + * deps: path-to-regexp@0.1.10 + - Adds support for named matching groups in the routes using a regex + - Adds backtracking protection to parameters without regexes defined + * deps: encodeurl@~2.0.0 + - Removes encoding of `\`, `|`, and `^` to align better with URL spec + * Deprecate passing `options.maxAge` and `options.expires` to `res.clearCookie` + - Will be ignored in v5, clearCookie will set a cookie with an expires in the past to instruct clients to delete the cookie + +4.19.2 / 2024-03-25 +========== + + * Improved fix for open redirect allow list bypass + 4.19.1 / 2024-03-20 ========== diff --git a/Readme.md b/Readme.md index d0f3cf56e6d..bc108d55fc0 100644 --- a/Readme.md +++ b/Readme.md @@ -1,10 +1,29 @@ [![Express Logo](https://i.cloudup.com/zfY6lL7eFa-3000x3000.png)](http://expressjs.com/) - Fast, unopinionated, minimalist web framework for [Node.js](http://nodejs.org). +**Fast, unopinionated, minimalist web framework for [Node.js](http://nodejs.org).** + +**This project has a [Code of Conduct][].** + +## Table of contents + +* [Installation](#Installation) +* [Features](#Features) +* [Docs & Community](#docs--community) +* [Quick Start](#Quick-Start) +* [Running Tests](#Running-Tests) +* [Philosophy](#Philosophy) +* [Examples](#Examples) +* [Contributing to Express](#Contributing) +* [TC (Technical Committee)](#tc-technical-committee) +* [Triagers](#triagers) +* [License](#license) + + +[![NPM Version][npm-version-image]][npm-url] +[![NPM Install Size][npm-install-size-image]][npm-install-size-url] +[![NPM Downloads][npm-downloads-image]][npm-downloads-url] +[![OpenSSF Scorecard Badge][ossf-scorecard-badge]][ossf-scorecard-visualizer] - [![NPM Version][npm-version-image]][npm-url] - [![NPM Install Size][npm-install-size-image]][npm-install-size-url] - [![NPM Downloads][npm-downloads-image]][npm-downloads-url] ```js const express = require('express') @@ -144,10 +163,82 @@ $ npm test The original author of Express is [TJ Holowaychuk](https://github.com/tj) -The current lead maintainer is [Douglas Christopher Wilson](https://github.com/dougwilson) - [List of all contributors](https://github.com/expressjs/express/graphs/contributors) +### TC (Technical Committee) + +* [UlisesGascon](https://github.com/UlisesGascon) - **Ulises Gascón** (he/him) +* [jonchurch](https://github.com/jonchurch) - **Jon Church** +* [wesleytodd](https://github.com/wesleytodd) - **Wes Todd** +* [LinusU](https://github.com/LinusU) - **Linus Unnebäck** +* [blakeembrey](https://github.com/blakeembrey) - **Blake Embrey** +* [sheplu](https://github.com/sheplu) - **Jean Burellier** +* [crandmck](https://github.com/crandmck) - **Rand McKinney** +* [ctcpip](https://github.com/ctcpip) - **Chris de Almeida** + +
+TC emeriti members + +#### TC emeriti members + + * [dougwilson](https://github.com/dougwilson) - **Douglas Wilson** + * [hacksparrow](https://github.com/hacksparrow) - **Hage Yaapa** + * [jonathanong](https://github.com/jonathanong) - **jongleberry** + * [niftylettuce](https://github.com/niftylettuce) - **niftylettuce** + * [troygoode](https://github.com/troygoode) - **Troy Goode** +
+ + +### Triagers + +* [aravindvnair99](https://github.com/aravindvnair99) - **Aravind Nair** +* [carpasse](https://github.com/carpasse) - **Carlos Serrano** +* [CBID2](https://github.com/CBID2) - **Christine Belzie** +* [enyoghasim](https://github.com/enyoghasim) - **David Enyoghasim** +* [UlisesGascon](https://github.com/UlisesGascon) - **Ulises Gascón** (he/him) +* [mertcanaltin](https://github.com/mertcanaltin) - **Mert Can Altin** +* [0ss](https://github.com/0ss) - **Salah** +* [import-brain](https://github.com/import-brain) - **Eric Cheng** (he/him) +* [3imed-jaberi](https://github.com/3imed-jaberi) - **Imed Jaberi** +* [dakshkhetan](https://github.com/dakshkhetan) - **Daksh Khetan** (he/him) +* [lucasraziel](https://github.com/lucasraziel) - **Lucas Soares Do Rego** +* [IamLizu](https://github.com/IamLizu) - **S M Mahmudul Hasan** (he/him) +* [Sushmeet](https://github.com/Sushmeet) - **Sushmeet Sunger** + +
+Triagers emeriti members + +#### Emeritus Triagers + + * [AuggieH](https://github.com/AuggieH) - **Auggie Hudak** + * [G-Rath](https://github.com/G-Rath) - **Gareth Jones** + * [MohammadXroid](https://github.com/MohammadXroid) - **Mohammad Ayashi** + * [NawafSwe](https://github.com/NawafSwe) - **Nawaf Alsharqi** + * [NotMoni](https://github.com/NotMoni) - **Moni** + * [VigneshMurugan](https://github.com/VigneshMurugan) - **Vignesh Murugan** + * [davidmashe](https://github.com/davidmashe) - **David Ashe** + * [digitaIfabric](https://github.com/digitaIfabric) - **David** + * [e-l-i-s-e](https://github.com/e-l-i-s-e) - **Elise Bonner** + * [fed135](https://github.com/fed135) - **Frederic Charette** + * [firmanJS](https://github.com/firmanJS) - **Firman Abdul Hakim** + * [getspooky](https://github.com/getspooky) - **Yasser Ameur** + * [ghinks](https://github.com/ghinks) - **Glenn** + * [ghousemohamed](https://github.com/ghousemohamed) - **Ghouse Mohamed** + * [gireeshpunathil](https://github.com/gireeshpunathil) - **Gireesh Punathil** + * [jake32321](https://github.com/jake32321) - **Jake Reed** + * [jonchurch](https://github.com/jonchurch) - **Jon Church** + * [lekanikotun](https://github.com/lekanikotun) - **Troy Goode** + * [marsonya](https://github.com/marsonya) - **Lekan Ikotun** + * [mastermatt](https://github.com/mastermatt) - **Matt R. Wilson** + * [maxakuru](https://github.com/maxakuru) - **Max Edell** + * [mlrawlings](https://github.com/mlrawlings) - **Michael Rawlings** + * [rodion-arr](https://github.com/rodion-arr) - **Rodion Abdurakhimov** + * [sheplu](https://github.com/sheplu) - **Jean Burellier** + * [tarunyadav1](https://github.com/tarunyadav1) - **Tarun yadav** + * [tunniclm](https://github.com/tunniclm) - **Mike Tunnicliffe** +
+ + ## License [MIT](LICENSE) @@ -164,3 +255,6 @@ The current lead maintainer is [Douglas Christopher Wilson](https://github.com/d [npm-install-size-url]: https://packagephobia.com/result?p=express [npm-url]: https://npmjs.org/package/express [npm-version-image]: https://badgen.net/npm/v/express +[ossf-scorecard-badge]: https://api.scorecard.dev/projects/github.com/expressjs/express/badge +[ossf-scorecard-visualizer]: https://ossf.github.io/scorecard-visualizer/#/projects/github.com/expressjs/express +[Code of Conduct]: https://github.com/expressjs/express/blob/master/Code-Of-Conduct.md diff --git a/Release-Process.md b/Release-Process.md index 55e62189250..9ca0a15ab46 100644 --- a/Release-Process.md +++ b/Release-Process.md @@ -77,6 +77,13 @@ non-patch flow. - This branch contains the commits accepted so far that implement the proposal in the tracking pull request. +### Pre-release Versions + +Alpha and Beta releases are made from a proposal branch. The version number should be +incremented to the next minor version with a `-beta` or `-alpha` suffix. +For example, if the next beta release is `5.0.1`, the beta release would be `5.0.1-beta.0`. +The pre-releases are unstable and not suitable for production use. + ### Patch flow In the patch flow, simple changes are committed to the release branch which diff --git a/Security.md b/Security.md index cdcd7a6e0aa..dcfbe88abd4 100644 --- a/Security.md +++ b/Security.md @@ -29,6 +29,12 @@ announcement, and may ask for additional information or guidance. Report security bugs in third-party modules to the person or team maintaining the module. +## Pre-release Versions + +Alpha and Beta releases are unstable and **not suitable for production use**. +Vulnerabilities found in pre-releases should be reported according to the [Reporting a Bug](#reporting-a-bug) section. +Due to the unstable nature of the branch it is not guaranteed that any fixes will be released in the next pre-release. + ## Disclosure Policy When the security team receives a security bug report, they will assign it to a @@ -40,6 +46,10 @@ involving the following steps: * Prepare fixes for all releases still under maintenance. These fixes will be released as fast as possible to npm. +## The Express Threat Model + +We are currently working on a new version of the security model, the most updated version can be found [here](https://github.com/expressjs/security-wg/blob/main/docs/ThreatModel.md) + ## Comments on this Policy If you have suggestions on how this process could be improved please submit a diff --git a/Triager-Guide.md b/Triager-Guide.md index a2909ef30db..c15e6be5313 100644 --- a/Triager-Guide.md +++ b/Triager-Guide.md @@ -9,11 +9,18 @@ classification: * `needs triage`: This can be kept if the triager is unsure which next steps to take * `awaiting more info`: If more info has been requested from the author, apply this label. -* `question`: User questions that do not appear to be bugs or enhancements. -* `discuss`: Topics for discussion. Might end in an `enhancement` or `question` label. * `bug`: Issues that present a reasonable conviction there is a reproducible bug. * `enhancement`: Issues that are found to be a reasonable candidate feature additions. +If the issue is a question or discussion, it should be moved to GitHub Discussions. + +### Moving Discussions and Questions to GitHub Discussions + +For issues labeled with `question` or `discuss`, it is recommended to move them to GitHub Discussions instead: + +* **Questions**: User questions that do not appear to be bugs or enhancements should be moved to GitHub Discussions. +* **Discussions**: Topics for discussion should be moved to GitHub Discussions. If the discussion leads to a new feature or bug identification, it can be moved back to Issues. + In all cases, issues may be closed by maintainers if they don't receive a timely response when further information is sought, or when additional questions are asked. diff --git a/appveyor.yml b/appveyor.yml index ce26523b3aa..629499bf37c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -23,6 +23,7 @@ environment: - nodejs_version: "19.9" - nodejs_version: "20.11" - nodejs_version: "21.6" + - nodejs_version: "22.0" cache: - node_modules install: diff --git a/lib/response.js b/lib/response.js index 6fddbf35165..76b6b54a3b8 100644 --- a/lib/response.js +++ b/lib/response.js @@ -34,7 +34,6 @@ var extname = path.extname; var mime = send.mime; var resolve = path.resolve; var vary = require('vary'); -var urlParse = require('url').parse; /** * Response prototype. @@ -823,6 +822,14 @@ res.get = function(field){ */ res.clearCookie = function clearCookie(name, options) { + if (options) { + if (options.maxAge) { + deprecate('res.clearCookie: Passing "options.maxAge" is deprecated. In v5.0.0 of Express, this option will be ignored, as res.clearCookie will automatically set cookies to expire immediately. Please update your code to omit this option.'); + } + if (options.expires) { + deprecate('res.clearCookie: Passing "options.expires" is deprecated. In v5.0.0 of Express, this option will be ignored, as res.clearCookie will automatically set cookies to expire immediately. Please update your code to omit this option.'); + } + } var opts = merge({ expires: new Date(1), path: '/' }, options); return this.cookie(name, '', opts); @@ -905,32 +912,16 @@ res.cookie = function (name, value, options) { */ res.location = function location(url) { - var loc = String(url); + var loc; // "back" is an alias for the referrer if (url === 'back') { loc = this.req.get('Referrer') || '/'; + } else { + loc = String(url); } - var lowerLoc = loc.toLowerCase(); - var encodedUrl = encodeUrl(loc); - if (lowerLoc.indexOf('https://') === 0 || lowerLoc.indexOf('http://') === 0) { - try { - var parsedUrl = urlParse(loc); - var parsedEncodedUrl = urlParse(encodedUrl); - // Because this can encode the host, check that we did not change the host - if (parsedUrl.host !== parsedEncodedUrl.host) { - // If the host changes after encodeUrl, return the original url - return this.set('Location', loc); - } - } catch (e) { - // If parse fails, return the original url - return this.set('Location', loc); - } - } - - // set location - return this.set('Location', encodedUrl); + return this.set('Location', encodeUrl(loc)); }; /** @@ -978,7 +969,7 @@ res.redirect = function redirect(url) { html: function(){ var u = escapeHtml(address); - body = '

' + statuses.message[status] + '. Redirecting to ' + u + '

' + body = '

' + statuses.message[status] + '. Redirecting to ' + u + '

' }, default: function(){ diff --git a/package.json b/package.json index 51c6aba2127..bffa70a6f1c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "express", "description": "Fast, unopinionated, minimalist web framework", - "version": "4.19.1", + "version": "4.20.0", "author": "TJ Holowaychuk ", "contributors": [ "Aaron Heckmann ", @@ -30,30 +30,30 @@ "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.2.0", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", "qs": "6.11.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.0", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -91,8 +91,8 @@ "scripts": { "lint": "eslint .", "test": "mocha --require test/support/env --reporter spec --bail --check-leaks test/ test/acceptance/", - "test-ci": "nyc --reporter=lcovonly --reporter=text npm test", - "test-cov": "nyc --reporter=html --reporter=text npm test", + "test-ci": "nyc --exclude examples --exclude test --exclude benchmarks --reporter=lcovonly --reporter=text npm test", + "test-cov": "nyc --exclude examples --exclude test --exclude benchmarks --reporter=html --reporter=text npm test", "test-tap": "mocha --require test/support/env --reporter tap --check-leaks test/ test/acceptance/" } } diff --git a/test/app.router.js b/test/app.router.js index 12b6c1fa519..8e427bd6dc7 100644 --- a/test/app.router.js +++ b/test/app.router.js @@ -6,6 +6,8 @@ var express = require('../') , assert = require('assert') , methods = require('methods'); +var shouldSkipQuery = require('./support/utils').shouldSkipQuery + describe('app.router', function(){ it('should restore req.params after leaving router', function(done){ var app = express(); @@ -39,6 +41,9 @@ describe('app.router', function(){ if (method === 'connect') return; it('should include ' + method.toUpperCase(), function(done){ + if (method === 'query' && shouldSkipQuery(process.versions.node)) { + this.skip() + } var app = express(); app[method]('/foo', function(req, res){ @@ -188,6 +193,23 @@ describe('app.router', function(){ .expect('editing user 10', done); }) + if (supportsRegexp('(?.*)')) { + it('should populate req.params with named captures', function(done){ + var app = express(); + var re = new RegExp('^/user/(?[0-9]+)/(view|edit)?$'); + + app.get(re, function(req, res){ + var id = req.params.userId + , op = req.params[0]; + res.end(op + 'ing user ' + id); + }); + + request(app) + .get('/user/10/edit') + .expect('editing user 10', done); + }) + } + it('should ensure regexp matches path prefix', function (done) { var app = express() var p = [] @@ -1109,3 +1131,12 @@ describe('app.router', function(){ assert.strictEqual(app.get('/', function () {}), app) }) }) + +function supportsRegexp(source) { + try { + new RegExp(source) + return true + } catch (e) { + return false + } +} diff --git a/test/express.static.js b/test/express.static.js index 245fd5929cc..23e607ed933 100644 --- a/test/express.static.js +++ b/test/express.static.js @@ -486,7 +486,7 @@ describe('express.static()', function () { request(this.app) .get('/users') .expect('Location', '/users/') - .expect(301, //, done) + .expect(301, /\/users\//, done) }) it('should redirect directories with query string', function (done) { @@ -508,7 +508,7 @@ describe('express.static()', function () { .get('/snow') .expect('Location', '/snow%20%E2%98%83/') .expect('Content-Type', /html/) - .expect(301, />Redirecting to \/snow%20%E2%98%83\/<\/a>Redirecting to \/snow%20%E2%98%83\/ { }').end(); + }); + + request(app) + .get('/') + .expect('Location', 'data:text/javascript,export%20default%20()%20=%3E%20%7B%20%7D') + .expect(200, done) + }) + + it('should consistently handle non-string input: boolean', function (done) { + var app = express() + app.use(function (req, res) { + res.location(true).end(); + }); + + request(app) + .get('/') + .expect('Location', 'true') + .expect(200, done) + }); + + it('should consistently handle non-string inputs: object', function (done) { + var app = express() + app.use(function (req, res) { + res.location({}).end(); + }); + + request(app) + .get('/') + .expect('Location', '[object%20Object]') + .expect(200, done) + }); + + it('should consistently handle non-string inputs: array', function (done) { + var app = express() + app.use(function (req, res) { + res.location([]).end(); + }); + + request(app) + .get('/') + .expect('Location', '') + .expect(200, done) + }); + + it('should consistently handle empty string input', function (done) { + var app = express() + app.use(function (req, res) { + res.location('').end(); + }); + + request(app) + .get('/') + .expect('Location', '') + .expect(200, done) + }); + + if (typeof URL !== 'undefined') { it('should accept an instance of URL', function (done) { var app = express(); @@ -161,4 +180,183 @@ describe('res', function(){ }); } }) + + describe('location header encoding', function() { + function createRedirectServerForDomain (domain) { + var app = express(); + app.use(function (req, res) { + var host = url.parse(req.query.q, false, true).host; + // This is here to show a basic check one might do which + // would pass but then the location header would still be bad + if (host !== domain) { + res.status(400).end('Bad host: ' + host + ' !== ' + domain); + } + res.location(req.query.q).end(); + }); + return app; + } + + function testRequestedRedirect (app, inputUrl, expected, expectedHost, done) { + return request(app) + // Encode uri because old supertest does not and is required + // to test older node versions. New supertest doesn't re-encode + // so this works in both. + .get('/?q=' + encodeURIComponent(inputUrl)) + .expect('') // No body. + .expect(200) + .expect('Location', expected) + .end(function (err, res) { + if (err) { + console.log('headers:', res.headers) + console.error('error', res.error, err); + return done(err, res); + } + + // Parse the hosts from the input URL and the Location header + var inputHost = url.parse(inputUrl, false, true).host; + var locationHost = url.parse(res.headers['location'], false, true).host; + + assert.strictEqual(locationHost, expectedHost); + + // Assert that the hosts are the same + if (inputHost !== locationHost) { + return done(new Error('Hosts do not match: ' + inputHost + " !== " + locationHost)); + } + + return done(null, res); + }); + } + + it('should not touch already-encoded sequences in "url"', function (done) { + var app = createRedirectServerForDomain('google.com'); + testRequestedRedirect( + app, + 'https://google.com?q=%A710', + 'https://google.com?q=%A710', + 'google.com', + done + ); + }); + + it('should consistently handle relative urls', function (done) { + var app = createRedirectServerForDomain(null); + testRequestedRedirect( + app, + '/foo/bar', + '/foo/bar', + null, + done + ); + }); + + it('should not encode urls in such a way that they can bypass redirect allow lists', function (done) { + var app = createRedirectServerForDomain('google.com'); + testRequestedRedirect( + app, + 'http://google.com\\@apple.com', + 'http://google.com\\@apple.com', + 'google.com', + done + ); + }); + + it('should not be case sensitive', function (done) { + var app = createRedirectServerForDomain('google.com'); + testRequestedRedirect( + app, + 'HTTP://google.com\\@apple.com', + 'HTTP://google.com\\@apple.com', + 'google.com', + done + ); + }); + + it('should work with https', function (done) { + var app = createRedirectServerForDomain('google.com'); + testRequestedRedirect( + app, + 'https://google.com\\@apple.com', + 'https://google.com\\@apple.com', + 'google.com', + done + ); + }); + + it('should correctly encode schemaless paths', function (done) { + var app = createRedirectServerForDomain('google.com'); + testRequestedRedirect( + app, + '//google.com\\@apple.com/', + '//google.com\\@apple.com/', + 'google.com', + done + ); + }); + + it('should keep backslashes in the path', function (done) { + var app = createRedirectServerForDomain('google.com'); + testRequestedRedirect( + app, + 'https://google.com/foo\\bar\\baz', + 'https://google.com/foo\\bar\\baz', + 'google.com', + done + ); + }); + + it('should escape header splitting for old node versions', function (done) { + var app = createRedirectServerForDomain('google.com'); + testRequestedRedirect( + app, + 'http://google.com\\@apple.com/%0d%0afoo:%20bar', + 'http://google.com\\@apple.com/%0d%0afoo:%20bar', + 'google.com', + done + ); + }); + + it('should encode unicode correctly', function (done) { + var app = createRedirectServerForDomain(null); + testRequestedRedirect( + app, + '/%e2%98%83', + '/%e2%98%83', + null, + done + ); + }); + + it('should encode unicode correctly even with a bad host', function (done) { + var app = createRedirectServerForDomain('google.com'); + testRequestedRedirect( + app, + 'http://google.com\\@apple.com/%e2%98%83', + 'http://google.com\\@apple.com/%e2%98%83', + 'google.com', + done + ); + }); + + it('should work correctly despite using deprecated url.parse', function (done) { + var app = createRedirectServerForDomain('google.com'); + testRequestedRedirect( + app, + 'https://google.com\'.bb.com/1.html', + 'https://google.com\'.bb.com/1.html', + 'google.com', + done + ); + }); + + it('should encode file uri path', function (done) { + var app = createRedirectServerForDomain(''); + testRequestedRedirect( + app, + 'file:///etc\\passwd', + 'file:///etc\\passwd', + '', + done + ); + }); + }); }) diff --git a/test/res.redirect.js b/test/res.redirect.js index 5ffc7e48f12..f7214d93312 100644 --- a/test/res.redirect.js +++ b/test/res.redirect.js @@ -106,7 +106,7 @@ describe('res', function(){ .set('Accept', 'text/html') .expect('Content-Type', /html/) .expect('Location', 'http://google.com') - .expect(302, '

Found. Redirecting to http://google.com

', done) + .expect(302, '

Found. Redirecting to http://google.com

', done) }) it('should escape the url', function(done){ @@ -122,9 +122,27 @@ describe('res', function(){ .set('Accept', 'text/html') .expect('Content-Type', /html/) .expect('Location', '%3Cla\'me%3E') - .expect(302, '

Found. Redirecting to %3Cla'me%3E

', done) + .expect(302, '

Found. Redirecting to %3Cla'me%3E

', done) }) + it('should not render evil javascript links in anchor href (prevent XSS)', function(done){ + var app = express(); + var xss = 'javascript:eval(document.body.innerHTML=`

XSS

`);'; + var encodedXss = 'javascript:eval(document.body.innerHTML=%60%3Cp%3EXSS%3C/p%3E%60);'; + + app.use(function(req, res){ + res.redirect(xss); + }); + + request(app) + .get('/') + .set('Host', 'http://example.com') + .set('Accept', 'text/html') + .expect('Content-Type', /html/) + .expect('Location', encodedXss) + .expect(302, '

Found. Redirecting to ' + encodedXss +'

', done); + }); + it('should include the redirect type', function(done){ var app = express(); @@ -137,7 +155,7 @@ describe('res', function(){ .set('Accept', 'text/html') .expect('Content-Type', /html/) .expect('Location', 'http://google.com') - .expect(301, '

Moved Permanently. Redirecting to http://google.com

', done); + .expect(301, '

Moved Permanently. Redirecting to http://google.com

', done); }) }) diff --git a/test/res.send.js b/test/res.send.js index c92568db6ad..b4cf68a7df6 100644 --- a/test/res.send.js +++ b/test/res.send.js @@ -7,6 +7,8 @@ var methods = require('methods'); var request = require('supertest'); var utils = require('./support/utils'); +var shouldSkipQuery = require('./support/utils').shouldSkipQuery + describe('res', function(){ describe('.send()', function(){ it('should set body to ""', function(done){ @@ -409,6 +411,9 @@ describe('res', function(){ if (method === 'connect') return; it('should send ETag in response to ' + method.toUpperCase() + ' request', function (done) { + if (method === 'query' && shouldSkipQuery(process.versions.node)) { + this.skip() + } var app = express(); app[method]('/', function (req, res) { diff --git a/test/support/utils.js b/test/support/utils.js index 5b4062e0b2a..5ad4ca98410 100644 --- a/test/support/utils.js +++ b/test/support/utils.js @@ -16,6 +16,7 @@ exports.shouldHaveBody = shouldHaveBody exports.shouldHaveHeader = shouldHaveHeader exports.shouldNotHaveBody = shouldNotHaveBody exports.shouldNotHaveHeader = shouldNotHaveHeader; +exports.shouldSkipQuery = shouldSkipQuery /** * Assert that a supertest response has a specific body. @@ -70,3 +71,16 @@ function shouldNotHaveHeader(header) { assert.ok(!(header.toLowerCase() in res.headers), 'should not have header ' + header); }; } + +function getMajorVersion(versionString) { + return versionString.split('.')[0]; +} + +function shouldSkipQuery(versionString) { + // Skipping HTTP QUERY tests on Node 21, it is reported in http.METHODS on 21.7.2 but not supported + // update this implementation to run on supported versions of 21 once they exist + // upstream tracking https://github.com/nodejs/node/issues/51562 + // express tracking issue: https://github.com/expressjs/express/issues/5615 + return Number(getMajorVersion(versionString)) === 21 +} + pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy