Skip to content

feat: support custom pathToRegexpModule #102

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

Merged
merged 1 commit into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions app/middleware/securities.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ module.exports = (_, app) => {
}

// format csrf.cookieDomain
const orginalCookieDomain = options.csrf.cookieDomain;
if (orginalCookieDomain && typeof orginalCookieDomain !== 'function') {
options.csrf.cookieDomain = () => orginalCookieDomain;
const originalCookieDomain = options.csrf.cookieDomain;
if (originalCookieDomain && typeof originalCookieDomain !== 'function') {
options.csrf.cookieDomain = () => originalCookieDomain;
}

defaultMiddleware.forEach(middlewareName => {
Expand Down Expand Up @@ -46,7 +46,10 @@ module.exports = (_, app) => {
app.deprecate('[egg-security] Please use `config.security.xframe.ignore` instead, `config.security.xframe.blackUrls` will be removed very soon');
opt.ignore = opt.blackUrls;
}
opt.matching = createMatch(opt);
opt.matching = createMatch({
...opt,
pathToRegexpModule: app.options.pathToRegexpModule,
});

const fn = require(path.join(__dirname, '../../lib/middlewares', middlewareName))(opt, app);
middlewares.push(fn);
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"@eggjs/ip": "^2.0.2",
"csrf": "^3.0.6",
"delegates": "^1.0.0",
"egg-path-matching": "^1.0.0",
"egg-path-matching": "^1.2.0",
"escape-html": "^1.0.3",
"extend": "^3.0.1",
"koa-compose": "^4.1.0",
Expand All @@ -43,7 +43,8 @@
"eslint": "^8.40.0",
"eslint-config-egg": "^12.2.1",
"spy": "^1.0.0",
"supertest": "^6.3.3"
"supertest": "^6.3.3",
"path-to-regexp-v8": "npm:path-to-regexp@8"
},
"scripts": {
"lint": "eslint .",
Expand Down
20 changes: 20 additions & 0 deletions test/fixtures/apps/iframe-with-pathToRegexpModule/app/router.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module.exports = function(app) {
app.get('/', controller);
app.get('/foo', controller);
app.get('/hello', controller);
app.get('/hello/other/world', controller);
app.get('/world/12', controller);

app.get('/options', options, controller);

async function controller() {
this.body = 'body';
}

async function options(ctx, next) {
ctx.securityOptions.xframe = {
value: 'ALLOW-FROM http://www.domain.com',
};
return next();
}
Comment on lines +14 to +19
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Review security implications of ALLOW-FROM directive.

The middleware sets X-Frame-Options to ALLOW-FROM http://www.domain.com. Note that:

  1. ALLOW-FROM is deprecated in modern browsers
  2. Consider using CSP frame-ancestors instead

Consider updating to use Content-Security-Policy:

 async function options(ctx, next) {
-  ctx.securityOptions.xframe = {
-    value: 'ALLOW-FROM http://www.domain.com',
-  };
+  ctx.securityOptions.csp = {
+    value: "frame-ancestors 'self' http://www.domain.com"
+  };
   return next();
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function options(ctx, next) {
ctx.securityOptions.xframe = {
value: 'ALLOW-FROM http://www.domain.com',
};
return next();
}
async function options(ctx, next) {
ctx.securityOptions.csp = {
value: "frame-ancestors 'self' http://www.domain.com"
};
return next();
}

};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
exports.keys = 'test key';

exports.security = {
defaultMiddleware: 'xframe',
xframe: {
ignore: ['/hello', '/world/:id'],
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "iframe-with-pathToRegexpModule"
}
125 changes: 125 additions & 0 deletions test/xframe-with-pathToRegexpModule.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
const { strict: assert } = require('node:assert');
const mm = require('egg-mock');

describe('test/xframe-with-pathToRegexpModule.test.js', () => {
let app;
let app2;
let app3;
let app4;
before(async () => {
app = mm.app({
baseDir: 'apps/iframe-with-pathToRegexpModule',
plugin: 'security',
pathToRegexpModule: require.resolve('path-to-regexp-v8'),
});
await app.ready();

app2 = mm.app({
baseDir: 'apps/iframe-novalue',
plugin: 'security',
pathToRegexpModule: require.resolve('path-to-regexp-v8'),
});
await app2.ready();

app3 = mm.app({
baseDir: 'apps/iframe-allowfrom',
plugin: 'security',
pathToRegexpModule: require.resolve('path-to-regexp-v8'),
});
await app3.ready();

app4 = mm.app({
baseDir: 'apps/iframe-black-urls',
plugin: 'security',
pathToRegexpModule: require.resolve('path-to-regexp-v8'),
});
await app4.ready();
});

afterEach(mm.restore);

it('should contain X-Frame-Options: SAMEORIGIN', async () => {
await app.httpRequest()
.get('/')
.set('accept', 'text/html')
.expect('x-frame-options', 'SAMEORIGIN');

await app.httpRequest()
.get('/foo')
.set('accept', 'text/html')
.expect('x-frame-options', 'SAMEORIGIN');
});

it('should contain X-Frame-Options: ALLOW-FROM http://www.domain.com by this.securityOptions', async () => {
const res = await app.httpRequest()
.get('/options')
.set('accept', 'text/html');
assert.equal(res.status, 200);
assert.equal(res.headers['x-frame-options'], 'ALLOW-FROM http://www.domain.com');
});

it('should contain X-Frame-Options: SAMEORIGIN when dont set value option', function(done) {
app2.httpRequest()
.get('/foo')
.set('accept', 'text/html')
.expect('x-frame-options', 'SAMEORIGIN', done);
});

it('should contain X-Frame-Options: ALLOW-FROM with page when set ALLOW-FROM and page option', function(done) {
app3.httpRequest()
.get('/foo')
.set('accept', 'text/html')
.expect('x-frame-options', 'ALLOW-FROM http://www.domain.com', done);
});

it('should not contain X-Frame-Options: SAMEORIGIN when use ignore', async () => {
let res = await app.httpRequest()
.get('/hello')
.set('accept', 'text/html')
.expect(200);
assert.equal(res.headers['x-frame-options'], undefined);

// '/hello' won't match '/hello/other/world' on path-to-regexp@8
res = await app.httpRequest()
.get('/hello/other/world')
.set('accept', 'text/html')
.expect(200);
assert.equal(res.headers['x-frame-options'], 'SAMEORIGIN');
Comment on lines +82 to +87
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Breaking change in path-to-regexp v8 path matching needs documentation

The change in path matching behavior where '/hello' no longer matches '/hello/other/world' in path-to-regexp v8 is not documented in the CHANGELOG.md. This is a significant breaking change that could affect applications upgrading to the new version.

Please update the CHANGELOG.md to include:

  • The path matching behavior change in path-to-regexp v8
  • Examples of affected patterns
  • Migration guidance for users updating their routes
🔗 Analysis chain

Document breaking change in path matching behavior.

Important behavioral change noted in the test:

'/hello' won't match '/hello/other/world' on path-to-regexp@8

This is a breaking change that should be documented in the changelog.

Let's check if this is documented:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for changelog or documentation updates
rg -l "path-to-regexp.*8|breaking.*change" --type md

Length of output: 54


Script:

#!/bin/bash
# Check package.json for path-to-regexp version
fd package.json --type f --exec cat {} \; | jq -r 'select(.dependencies["path-to-regexp"] or .devDependencies["path-to-regexp"]) | {name: .name, dependencies: .dependencies["path-to-regexp"], devDependencies: .devDependencies["path-to-regexp"]}'

# Search for documentation files with broader patterns
fd -e md -e txt -e rst | grep -i -E 'change|upgrade|migration|release|history|breaking' || true

Length of output: 353


Script:

#!/bin/bash
# Check CHANGELOG.md content
echo "=== Checking CHANGELOG.md for path-to-regexp mentions ==="
rg -i "path-to-regexp" CHANGELOG.md -C 2 || true

echo -e "\n=== Checking for breaking changes sections ==="
rg "BREAKING|Breaking Changes" CHANGELOG.md -C 2 || true

Length of output: 386


res = await app4.httpRequest()
.get('/hello')
.set('accept', 'text/html')
.expect(200);
assert.equal(res.headers['x-frame-options'], undefined);

res = await app.httpRequest()
.get('/world/12')
.set('accept', 'text/html')
.expect(200);
assert.equal(res.headers['x-frame-options'], undefined);

res = await app.httpRequest()
.get('/world/12?xx=xx')
.set('accept', 'text/html')
.expect(200);
assert.equal(res.headers['x-frame-options'], undefined);

res = await app2.httpRequest()
.get('/hello')
.set('accept', 'text/html')
.expect(200);
assert.equal(res.headers['x-frame-options'], undefined);

res = await app2.httpRequest()
.get('/world/12')
.set('accept', 'text/html')
.expect(200);
assert.equal(res.headers['x-frame-options'], undefined);

res = await app2.httpRequest()
.get('/world/12?xx=xx')
.set('accept', 'text/html')
.expect(200);
assert.equal(res.headers['x-frame-options'], undefined);
});
});
28 changes: 17 additions & 11 deletions test/xframe.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,12 @@ describe('test/xframe.test.js', () => {
await app.httpRequest()
.get('/')
.set('accept', 'text/html')
.expect('X-Frame-Options', 'SAMEORIGIN');
.expect('x-frame-options', 'SAMEORIGIN');

await app.httpRequest()
.get('/foo')
.set('accept', 'text/html')
.expect('X-Frame-Options', 'SAMEORIGIN');
.expect('x-frame-options', 'SAMEORIGIN');
});

it('should contain X-Frame-Options: ALLOW-FROM http://www.domain.com by this.securityOptions', async () => {
Expand All @@ -58,57 +58,63 @@ describe('test/xframe.test.js', () => {
app2.httpRequest()
.get('/foo')
.set('accept', 'text/html')
.expect('X-Frame-Options', 'SAMEORIGIN', done);
.expect('x-frame-options', 'SAMEORIGIN', done);
});

it('should contain X-Frame-Options: ALLOW-FROM with page when set ALLOW-FROM and page option', function(done) {
app3.httpRequest()
.get('/foo')
.set('accept', 'text/html')
.expect('X-Frame-Options', 'ALLOW-FROM http://www.domain.com', done);
.expect('x-frame-options', 'ALLOW-FROM http://www.domain.com', done);
});

it('should not contain X-Frame-Options: SAMEORIGIN when use ignore', async () => {
let res = await app.httpRequest()
.get('/hello')
.set('accept', 'text/html')
.expect(200);
assert.equal(res.headers['X-Frame-Options'], undefined);
assert.equal(res.headers['x-frame-options'], undefined);

res = await app.httpRequest()
.get('/hello/other/world')
.set('accept', 'text/html')
.expect(200);
assert.equal(res.headers['x-frame-options'], undefined);

res = await app4.httpRequest()
.get('/hello')
.set('accept', 'text/html')
.expect(200);
assert.equal(res.headers['X-Frame-Options'], undefined);
assert.equal(res.headers['x-frame-options'], undefined);

res = await app.httpRequest()
.get('/world/12')
.set('accept', 'text/html')
.expect(200);
assert.equal(res.headers['X-Frame-Options'], undefined);
assert.equal(res.headers['x-frame-options'], undefined);

res = await app.httpRequest()
.get('/world/12?xx=xx')
.set('accept', 'text/html')
.expect(200);
assert.equal(res.headers['X-Frame-Options'], undefined);
assert.equal(res.headers['x-frame-options'], undefined);

res = await app2.httpRequest()
.get('/hello')
.set('accept', 'text/html')
.expect(200);
assert.equal(res.headers['X-Frame-Options'], undefined);
assert.equal(res.headers['x-frame-options'], undefined);

res = await app2.httpRequest()
.get('/world/12')
.set('accept', 'text/html')
.expect(200);
assert.equal(res.headers['X-Frame-Options'], undefined);
assert.equal(res.headers['x-frame-options'], undefined);

res = await app2.httpRequest()
.get('/world/12?xx=xx')
.set('accept', 'text/html')
.expect(200);
assert.equal(res.headers['X-Frame-Options'], undefined);
assert.equal(res.headers['x-frame-options'], undefined);
});
});
Loading