Skip to content

Commit

Permalink
f
Browse files Browse the repository at this point in the history
  • Loading branch information
fengmk2 committed Jan 17, 2025
1 parent 109ce71 commit 4e221d9
Show file tree
Hide file tree
Showing 51 changed files with 515 additions and 437 deletions.
10 changes: 5 additions & 5 deletions __snapshots__/csp.test.ts.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,6 @@ exports['test/csp.test.ts should ignore path 1'] = {
"value": "1; mode=block"
},
"csp": {
"ignore": [
"/api/",
{}
],
"enable": true,
"policy": {
"script-src": [
Expand All @@ -80,7 +76,11 @@ exports['test/csp.test.ts should ignore path 1'] = {
"'self'"
],
"report-uri": "http://pointman.domain.com/csp?app=csp"
}
},
"ignore": [
"/api/",
{}
]
},
"referrerPolicy": {
"enable": false,
Expand Down
10 changes: 5 additions & 5 deletions __snapshots__/csrf.test.ts.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
exports['test/csrf.test.ts should update form with csrf token 1'] = {
"ignore": [
{},
null
],
"enable": true,
"type": "ctoken",
"ignoreJSON": false,
Expand Down Expand Up @@ -30,7 +26,11 @@ exports['test/csrf.test.ts should update form with csrf token 1'] = {
"signed": false,
"httpOnly": false,
"overwrite": true
}
},
"ignore": [
{},
null
]
}

exports['test/csrf.test.ts apps/csrf-supported-requests-default-config should works without error because csrf = false override default config 1'] = {
Expand Down
4 changes: 4 additions & 0 deletions __snapshots__/xss.test.ts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
exports['test/xss.test.ts should set X-XSS-Protection header value 0 when config is number 0 1'] = {
"enable": true,
"value": 0
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"@arethetypeswrong/cli": "^0.17.1",
"@eggjs/bin": "7",
"@eggjs/mock": "^6.0.5",
"@eggjs/supertest": "^8.1.1",
"@eggjs/supertest": "^8.2.0",
"@eggjs/tsconfig": "1",
"@types/escape-html": "^1.0.4",
"@types/extend": "^3.0.4",
Expand Down
6 changes: 5 additions & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ export default class AgentBoot implements ILifecycleBoot {
const app = this.app;
app.config.coreMiddleware.push('securities');
// parse config and check if config is legal
app.config.security = SecurityConfig.parse(app.config.security);
const parsed = SecurityConfig.parse(app.config.security);
if (typeof app.config.security.csrf === 'boolean') {
// support old config: `config.security.csrf = false`
app.config.security.csrf = parsed.csrf;
}

if (app.config.security.csrf.enable) {
const { ignoreJSON } = app.config.security.csrf;
Expand Down
12 changes: 10 additions & 2 deletions src/app/extend/agent.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { EggCore } from '@eggjs/core';
import { safeCurlForApplication } from '../../lib/extend/safe_curl.js';
import {
safeCurlForApplication,
type HttpClientRequestURL,
type HttpClientOptions,
type HttpClientResponse,
} from '../../lib/extend/safe_curl.js';

export default class SecurityAgent extends EggCore {
safeCurl = safeCurlForApplication;
async safeCurl<T = any>(
url: HttpClientRequestURL, options?: HttpClientOptions): Promise<HttpClientResponse<T>> {
return await safeCurlForApplication<T>(this, url, options);
}
}
14 changes: 11 additions & 3 deletions src/app/extend/application.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { EggCore } from '@eggjs/core';
import { safeCurlForApplication } from '../../lib/extend/safe_curl.js';
import {
safeCurlForApplication,
type HttpClientRequestURL,
type HttpClientOptions,
type HttpClientResponse,
} from '../../lib/extend/safe_curl.js';

const INPUT_CSRF = '\r\n<input type="hidden" name="_csrf" value="{{ctx.csrf}}" /></form>';
const INJECTION_DEFENSE = '<!--for injection--><!--</html>--><!--for injection-->';
Expand Down Expand Up @@ -30,14 +35,17 @@ export default class SecurityApplication extends EggCore {
return INJECTION_DEFENSE + html + INJECTION_DEFENSE;
}

safeCurl = safeCurlForApplication;
async safeCurl<T = any>(
url: HttpClientRequestURL, options?: HttpClientOptions): Promise<HttpClientResponse<T>> {
return await safeCurlForApplication<T>(this, url, options);
}
}

declare module '@eggjs/core' {
interface EggCore {
injectCsrf(html: string): string;
injectNonce(html: string): string;
injectHijackingDefense(html: string): string;
safeCurl: typeof safeCurlForApplication;
safeCurl<T = any>(url: HttpClientRequestURL, options?: HttpClientOptions): Promise<HttpClientResponse<T>>;
}
}
13 changes: 9 additions & 4 deletions src/app/extend/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as utils from '../../lib/utils.js';
import type {
HttpClientRequestURL,
HttpClientOptions,
HttpClientRequestReturn,
HttpClientResponse,
} from '../../lib/extend/safe_curl.js';
import { SecurityConfig, SecurityHelperConfig } from '../../types.js';

Expand Down Expand Up @@ -258,8 +258,13 @@ export default class SecurityContext extends Context {
}
}

async safeCurl(url: HttpClientRequestURL, options?: HttpClientOptions): HttpClientRequestReturn {
return await this.app.safeCurl(url, options);
async safeCurl<T = any>(
url: HttpClientRequestURL, options?: HttpClientOptions): Promise<HttpClientResponse<T>> {
return await this.app.safeCurl<T>(url, options);
}

unsafeRedirect(url: string, alt?: string) {
this.response.unsafeRedirect(url, alt);
}
}

Expand All @@ -272,6 +277,6 @@ declare module '@eggjs/core' {
ensureCsrfSecret(rotate?: boolean): void;
rotateCsrfSecret(): void;
assertCsrf(): void;
safeCurl(url: HttpClientRequestURL, options?: HttpClientOptions): HttpClientRequestReturn;
safeCurl<T = any>(url: HttpClientRequestURL, options?: HttpClientOptions): Promise<HttpClientResponse<T>>;
}
}
7 changes: 5 additions & 2 deletions src/app/extend/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ export default class SecurityResponse extends KoaResponse {
* ctx.unsafeRedirect('http://www.domain.com');
* ```
*/
unsafeRedirect = unsafeRedirect;
unsafeRedirect(url: string, alt?: string) {
unsafeRedirect.call(this, url, alt);
}

// app.response.unsafeRedirect = app.response.redirect;
// delegate(app.context, 'response').method('unsafeRedirect');
Expand Down Expand Up @@ -48,7 +50,8 @@ export default class SecurityResponse extends KoaResponse {

// if begin with '/', it means an internal jump
if (url[0] === '/' && url[1] !== '\\') {
return this.unsafeRedirect(url, alt);
this.unsafeRedirect(url, alt);
return;
}

let urlObject: URL;
Expand Down
7 changes: 4 additions & 3 deletions src/config/config.default.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import z from 'zod';
import { Context } from '@eggjs/core';

const CSRFSupportRequestItem = z.object({
path: z.instanceof(RegExp),
Expand Down Expand Up @@ -38,7 +39,7 @@ export type SecurityMiddlewareName = z.infer<typeof SecurityMiddlewareName>;
/**
* (ctx) => boolean
*/
const IgnoreOrMatchHandler = z.function().args(z.any()).returns(z.boolean());
const IgnoreOrMatchHandler = z.function().args(z.instanceof(Context)).returns(z.boolean());
export type IgnoreOrMatchHandler = z.infer<typeof IgnoreOrMatchHandler>;

const IgnoreOrMatch = z.union([
Expand Down Expand Up @@ -154,7 +155,7 @@ export const SecurityConfig = z.object({
cookieDomain: z.union([
z.string(),
z.function()
.args(z.any())
.args(z.instanceof(Context))
.returns(z.string()),
]).optional(),
/**
Expand Down Expand Up @@ -278,7 +279,7 @@ export const SecurityConfig = z.object({
*
* Default to `'1; mode=block'`
*/
value: z.string().default('1; mode=block'),
value: z.coerce.string().default('1; mode=block'),
}).default({}),
/**
* content security policy config
Expand Down
21 changes: 9 additions & 12 deletions src/lib/extend/safe_curl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,29 @@ type HttpClient = EggCore['HttpClient'];
type HttpClientParameters = Parameters<HttpClient['prototype']['request']>;
export type HttpClientRequestURL = HttpClientParameters[0];
export type HttpClientOptions = HttpClientParameters[1] & { checkAddress?: SSRFCheckAddressFunction };
export type HttpClientRequestReturn = ReturnType<HttpClient['prototype']['request']>;
export type HttpClientResponse<T = any> = Awaited<ReturnType<HttpClient['prototype']['request']>> & { data: T };

/**
* safe curl with ssrf protect
* @param {String} url request url
* @param {Object} options request options
* @return {Promise} response
* safe curl with ssrf protection
*/
export async function safeCurlForApplication(this: EggCore, url: HttpClientRequestURL, options: HttpClientOptions = {}): HttpClientRequestReturn {
const ssrfConfig = this.config.security.ssrf;
export async function safeCurlForApplication<T = any>(app: EggCore, url: HttpClientRequestURL, options: HttpClientOptions = {}) {
const ssrfConfig = app.config.security.ssrf;
if (ssrfConfig?.checkAddress) {
options.checkAddress = ssrfConfig.checkAddress;
} else {
this.logger.warn('[@eggjs/security] please configure `config.security.ssrf` first');
app.logger.warn('[@eggjs/security] please configure `config.security.ssrf` first');
}

if (ssrfConfig?.checkAddress) {
let httpClient = this[SSRF_HTTPCLIENT] as ReturnType<EggCore['createHttpClient']>;
let httpClient = app[SSRF_HTTPCLIENT] as ReturnType<EggCore['createHttpClient']>;
// use the new httpClient init with checkAddress
if (!httpClient) {
httpClient = this[SSRF_HTTPCLIENT] = this.createHttpClient({
httpClient = app[SSRF_HTTPCLIENT] = app.createHttpClient({
checkAddress: ssrfConfig.checkAddress,
});
}
return await httpClient.request(url, options);
return await httpClient.request<T>(url, options);
}

return await this.curl(url, options);
return await app.curl<T>(url, options);
}
2 changes: 1 addition & 1 deletion src/lib/middlewares/methodnoallow.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { METHODS } from 'node:http';
import type { Context, Next } from '@eggjs/core';

const METHODS_NOT_ALLOWED = [ 'trace', 'track' ];
const METHODS_NOT_ALLOWED = [ 'TRACE', 'TRACK' ];
const safeHttpMethodsMap: Record<string, boolean> = {};

for (const method of METHODS) {
Expand Down
4 changes: 4 additions & 0 deletions src/lib/middlewares/referrerPolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export default (options: SecurityConfig['referrerPolicy']) => {

const opts = {
...options,
// check refererPolicy for backward compatibility
// typo on the old version
// @see https://github.com/eggjs/security/blob/e3408408adec5f8d009d37f75126ed082481d0ac/lib/middlewares/referrerPolicy.js#L21C59-L21C72
...(ctx.securityOptions as any).refererPolicy,
...ctx.securityOptions.referrerPolicy,
};
if (checkIfIgnore(opts, ctx)) return;
Expand Down
2 changes: 1 addition & 1 deletion src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { normalize } from 'node:path';
import matcher from 'matcher';
import * as IP from '@eggjs/ip';
import IP from '@eggjs/ip';
import { Context } from '@eggjs/core';
import type { PathMatchingFun } from 'egg-path-matching';
import { SecurityConfig } from '../types.js';
Expand Down
4 changes: 1 addition & 3 deletions test/fixtures/apps/hsts-nosub/app/router.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
'use strict';

module.exports = function(app) {
app.get('/', function *(){
app.get('/', function(){
this.body = '123';
});
};
8 changes: 3 additions & 5 deletions test/fixtures/apps/noopen/app/router.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
'use strict';

module.exports = function(app) {
app.get('/', function *(){
app.get('/', function(){
this.body = '123';
});

app.get('/disable', function *(){
app.get('/disable', function(){
this.securityOptions.noopen = { enable: false };
this.body = '123';
});
};
};
12 changes: 5 additions & 7 deletions test/fixtures/apps/nosniff/app/router.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
'use strict';

module.exports = function(app) {
app.get('/', function *(){
app.get('/', function() {
this.body = '123';
});

app.get('/disable', function *(){
app.get('/disable', function() {
this.securityOptions.nosniff = { enable: false };
this.body = '123';
});

app.get('/redirect', function *(){
app.get('/redirect', function() {
this.redirect('/');
});

app.get('/redirect301', function *(){
app.get('/redirect301', function() {
this.status = 301;
this.redirect('/');
});

app.get('/redirect307', function *(){
app.get('/redirect307', function() {
this.status = 307;
this.redirect('/');
});
Expand Down
13 changes: 13 additions & 0 deletions test/fixtures/apps/referrer-config-compatibility/app/router.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module.exports = function(app) {
app.get('/', function() {
this.body = '123';
});
app.get('/referrer', function() {
const policy = this.query.policy;
this.body = '123';
this.securityOptions.refererPolicy = {
enable: true,
value: policy
}
});
};
11 changes: 11 additions & 0 deletions test/fixtures/apps/referrer-config-compatibility/config/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use strict';

exports.keys = 'test key';

exports.security = {
defaultMiddleware: 'referrerPolicy',
referrerPolicy: {
value: 'origin',
enable: true
},
};
3 changes: 3 additions & 0 deletions test/fixtures/apps/referrer-config-compatibility/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "referrer-config"
}
10 changes: 4 additions & 6 deletions test/fixtures/apps/referrer-config/app/router.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
'use strict';

module.exports = function(app) {
app.get('/', function *(){
app.get('/', function() {
this.body = '123';
});
app.get('/referrer', function *(){
app.get('/referrer', function() {
const policy = this.query.policy;
this.body = '123';
this.securityOptions.refererPolicy = {
this.securityOptions.referrerPolicy = {
enable: true,
value: policy
}
});
};
};
6 changes: 2 additions & 4 deletions test/fixtures/apps/referrer/app/router.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
'use strict';

module.exports = function(app) {
app.get('/', function *(){
app.get('/', function(){
this.body = '123';
});
};
};
Loading

0 comments on commit 4e221d9

Please sign in to comment.