項目結(jié)構(gòu)實踐
- 組件式構(gòu)建解決方案
推薦: 通過組件構(gòu)建解決方案
- components
- orders
- users
- index.js
- user.js
- userService.js
避免: 按照技術(shù)角色對文件進(jìn)行分組
- controllers
- api.js
- home.js
- models
- order.js
- user.js
分層設(shè)計組件蔓罚,保持Express在特定的區(qū)域
每一個組件都應(yīng)該包含層級
,一個專注的用于接入網(wǎng)絡(luò),邏輯追逮,數(shù)據(jù)的概念
經(jīng)典的分層結(jié)構(gòu)霜威,將代碼分類 web, service,和DAL層谈喳。每一層次只和上一層的結(jié)構(gòu)打交道公共使用的工具封裝成npm包
和java的maven二方包類似使用易于設(shè)置環(huán)境變量,安全和分級的配置
配置文件類似egg.js 中production.config.js
,dev.config.js
等戈泼⌒銮荩可以通過比如rc, nconf, config 和 convict等幾種包完成。
錯誤處理最佳實踐
- 使用內(nèi)建的錯誤對象
比如
throw new Error("not Provieded");
或者構(gòu)建自定義的錯誤對象
//從node錯誤派生的集中錯誤對象
function appError(name, httpCode, description, isOperational) {
Error.call(this);
Error.captureStackTrace(this);
this.name = name;
//...在這賦值其它屬性
};
appError.prototype = Object.create(Error.prototype);
appError.prototype.constructor = appError;
module.exports.appError = appError;
區(qū)分運(yùn)行錯誤(runtimeError)和程序設(shè)計錯誤
集中處理錯誤大猛,并且在特殊情況產(chǎn)生時候扭倾,優(yōu)雅退出服務(wù)
process.on('unhandledRejection', (reason, p) => {
//我剛剛捕獲了一個未處理的promise rejection, 因為我們已經(jīng)有了對于未處理錯誤的后備的處理機(jī)制(見下面), 直接拋出,讓它來處理
throw reason;
});
process.on('uncaughtException', (error) => {
//我剛收到一個從未被處理的錯誤挽绩,現(xiàn)在處理它膛壹,并決定是否需要重啟應(yīng)用
errorManagement.handler.handleError(error);
if (!errorManagement.handler.isTrustedError(error))
process.exit(1);
});
安全最佳實踐
- DOS攻擊,可以使用cloud負(fù)載均衡唉堪,防火墻或者nginx等做限流處理模聋,對于小的應(yīng)用,可以使用限流中間件做限速限制
const http = require('http');
const redis = require('redis');
const { RateLimiterRedis } = require('rate-limiter-flexible');
const redisClient = redis.createClient({
enable_offline_queue: false,
});
// Maximum 20 requests per second
const rateLimiter = new RateLimiterRedis({
storeClient: redisClient,
points: 20,
duration: 1,
blockDuration: 2, // block for 2 seconds if consumed more than 20 points per second
});
http.createServer(async (req, res) => {
try {
const rateLimiterRes = await rateLimiter.consume(req.socket.remoteAddress);
// Some app logic here
res.writeHead(200);
res.end();
} catch {
res.writeHead(429);
res.end('Too Many Requests');
}
})
.listen(3000);
- 不要在配置文件或者源代碼中存儲文本機(jī)密信息唠亚,使用環(huán)境變量或者Docker Secrets等安全管理系統(tǒng)链方。
const azure = require('azure');
const apiKey = process.env.AZURE_STORAGE_KEY;
可以使用git commit來審計提交和提交消息,以便意外添加秘密趾撵,例如git-secrets.
- 使用ORM庫防止SQL注入的漏洞
- 通用安全最佳實踐集合
- 使用SSL/TLS 加密客戶端服務(wù)器連接侄柔,避免中間人攻擊,監(jiān)控用戶的行為
- 使用加密存儲信息
- 參考OWASP建議 https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#Implement_Proper_Password_Strength_Controls.22
調(diào)整HTTP響應(yīng)頭加強(qiáng)安全性
用程序應(yīng)該使用安全的header來防止攻擊者使用常見的攻擊方式占调,諸如跨站點(diǎn)腳本(XSS)、點(diǎn)擊劫持和其他惡意攻擊移剪【可海可以使用模塊,比如 helmet輕松進(jìn)行配置
參考:
https://github.com/goldbergyoni/nodebestpractices/blob/master/sections/security/secureheaders.md經(jīng)常自動檢查易受攻擊的依賴庫
在npm的生態(tài)系統(tǒng)中, 一個項目有許多依賴是很常見的纵苛。在找到新的漏洞時, 應(yīng)始終將依賴項保留在檢查中剿涮。使用工具言津,類似于npm audit 或者 snyk跟蹤、監(jiān)視和修補(bǔ)易受攻擊的依賴項取试。將這些工具與 CI 設(shè)置集成, 以便在將其上線之前捕捉到易受攻擊的依賴庫悬槽。避免使用Node.js的crypto庫處理密碼,使用Bcrypt
當(dāng)存儲用戶密碼的時候瞬浓,建議使用bcrypt npm module提供的自適應(yīng)哈希算法bcrypt初婆,而不是使用Node.js的crypto模塊。由于Math.random()
的可預(yù)測性猿棉,它也不應(yīng)該作為密碼或者令牌生成的一部分磅叛。轉(zhuǎn)義 HTML、JS 和 CSS 輸出
發(fā)送給瀏覽器的不受信任數(shù)據(jù)可能會被執(zhí)行, 而不是顯示, 這通常被稱為跨站點(diǎn)腳本(XSS)攻擊萨赁。使用專用庫將數(shù)據(jù)顯式標(biāo)記為不應(yīng)執(zhí)行的純文本內(nèi)容(例如:編碼弊琴、轉(zhuǎn)義),可以減輕這種問題杖爽。驗證傳入的JSON schemas
驗證傳入請求的body payload敲董,并確保其符合預(yù)期要求, 如果沒有, 則快速報錯。為了避免每個路由中繁瑣的驗證編碼, 您可以使用基于JSON的輕量級驗證架構(gòu)慰安,比如jsonschema or joi支持黑名單的JWT
當(dāng)使用JSON Web Tokens(例如, 通過Passport.js), 默認(rèn)情況下, 沒有任何機(jī)制可以從發(fā)出的令牌中撤消訪問權(quán)限臣缀。一旦發(fā)現(xiàn)了一些惡意用戶活動, 只要它們持有有效的標(biāo)記, 就無法阻止他們訪問系統(tǒng)。通過實現(xiàn)一個不受信任令牌的黑名單泻帮,并在每個請求上驗證精置,來減輕此問題。限制每個用戶允許的登錄請求
使用諸如/login,/admin之類的較高權(quán)限的路由而沒有使用速率限制锣杂。使得應(yīng)用程序面臨暴力密碼字典攻擊的風(fēng)險脂倦。可以基于請求屬性(比如IP) 或者主體參數(shù)比如電子郵件等元莫,限制允許嘗試的次數(shù)赖阻。
const maxWrongAttemptsByIPperDay = 100;
const maxConsecutiveFailsByUsernameAndIP = 10;
const limiterSlowBruteByIP = new RateLimiterRedis({
storeClient: redisClient,
keyPrefix: 'login_fail_ip_per_day',
points: maxWrongAttemptsByIPperDay,
duration: 60 * 60 * 24,
blockDuration: 60 * 60 * 24, // Block for 1 day, if 100 wrong attempts per day
});
const limiterConsecutiveFailsByUsernameAndIP = new RateLimiterRedis({
storeClient: redisClient,
keyPrefix: 'login_fail_consecutive_username_and_ip',
points: maxConsecutiveFailsByUsernameAndIP,
duration: 60 * 60 * 24 * 90, // Store number for 90 days since first fail
blockDuration: 60 * 60, // Block for 1 hour
});
- 使用非root用戶運(yùn)行Nodejs
Node.js作為一個具有無限權(quán)限的root用戶運(yùn)行,使得攻擊者在本地計算機(jī)獲取無限制的權(quán)利踱蠢,比如(改變iptable火欧,引流到他的服務(wù)器上)
但是有兩個常見場景需要root權(quán)限。
- 運(yùn)行特權(quán)端口比如80端口
- Docker容器默認(rèn)以root運(yùn)行茎截,建議使用nginx反向代理進(jìn)行轉(zhuǎn)發(fā)到Node程序苇侵,然后監(jiān)聽非特權(quán)端口。
非root權(quán)限制作dockerfile鏡像以
FROM node:latest
COPY package.json .
RUN npm install
COPY . .
EXPOSE 3000
USER node
CMD ["node", "server.js"]
- 使用反向代理或者中間件限制負(fù)載大小
請求Body的有效負(fù)載荷越大企锌,Node單線程就越難處理榆浓,使得攻擊者沒有大量請求(DOS/DDOS)情況下,可以先讓服務(wù)器掛掉撕攒。陡鹃,可以在邊緣上(SLB,防火墻)等限制請求body的大小烘浦。
http {
...
# Limit the body size for ALL incoming requests to 1 MB
client_max_body_size 1m;
}
server {
...
# Limit the body size for incoming requests to this specific server block to 1 MB
client_max_body_size 1m;
}
location /upload {
...
# Limit the body size for incoming requests to this route to 1 MB
client_max_body_size 1m;
}
避免JavaScript的eval聲明
evel允許允許時指定自定義javascript代碼,不僅僅是性能問題萍鲸,還是重要的安全問題闷叉,因為惡意的js代碼可能來源于用戶的輸入,防止惡意RegEx讓Node.js的單線程過載執(zhí)行
正則表達(dá)式在方便的同時脊阴,給javascript應(yīng)用造成真正的威脅握侧,特別在nodejs平臺。匹配文本的用戶輸入需要大量的CPU周期來處理蹬叭。偏向第三方的驗證包藕咏,比如validator.js,而不是采用正則秽五,或者使用safe-regex來檢測有問題的正則表達(dá)式孽查。使用變量避免模塊加載
避免使用被指定為參數(shù)的路徑變量導(dǎo)入(requiring/importing)另一個文件, 因為該變量可能源自用戶輸入。此規(guī)則可以擴(kuò)展到一般情況下的訪問文件(例如坦喘,fs.readFile())盲再,或者包含源自用戶輸入的動態(tài)變量的其他敏感資源。
// 不安全, 因為helperPath變量可能通過用戶輸入而改變
const uploadHelpers = require(helperPath);
// 安全
const uploadHelpers = require('./helpers/upload');
-
在沙箱中運(yùn)行不安全的代碼
例如, 考慮一個動態(tài)框架(如 webpack), 該框架接受自定義加載器(custom loaders), 并在構(gòu)建時動態(tài)執(zhí)行這些加載器瓣铣。在存在一些惡意插件的情況下, 我們希望最大限度地減少損害, 甚至可能讓工作流成功終止 - 這需要在一個沙箱環(huán)境中運(yùn)行插件, 該環(huán)境在資源答朋、宕機(jī)和我們共享的信息方面是完全隔離的。三個主要選項可以幫助實現(xiàn)這種隔離:
- 一個專門的子進(jìn)程 - 這提供了一個快速的信息隔離, 但要求制約子進(jìn)程, 限制其執(zhí)行時間, 并從錯誤中恢復(fù)
- 一個基于云的無服務(wù)框架滿足所有沙盒要求棠笑,但動態(tài)部署和調(diào)用Faas方法不是本部分的內(nèi)容
- 一些npm庫梦碗,比如sandbox和vm2允許通過一行代碼執(zhí)行隔離代碼。盡管后一種選擇在簡單中獲勝, 但它提供了有限的保護(hù)蓖救。
const Sandbox = require("sandbox");
const s = new Sandbox();
s.run( "lol)hai", function( output ) {
console.log(output);
//output='Syntax error'
});
// Example 4 - Restricted code
s.run( "process.platform", function( output ) {
console.log(output);
//output=Null
})
// Example 5 - Infinite loop
s.run( "while (true) {}", function( output ) {
console.log(output);
//output='Timeout'
})
- 隱藏客戶端的錯誤詳細(xì)信息
盡管子進(jìn)程非常棒, 但使用它們應(yīng)該謹(jǐn)慎洪规。如果無法避免傳遞用戶輸入,就必須經(jīng)過脫敏處理循捺。 未經(jīng)脫敏處理的輸入執(zhí)行系統(tǒng)級邏輯的危險是無限的, 從遠(yuǎn)程代碼執(zhí)行到暴露敏感的系統(tǒng)數(shù)據(jù), 甚至數(shù)據(jù)丟失斩例。準(zhǔn)備工作的檢查清單可能是這樣的
- 避免在每一種情況下的用戶輸入, 否則驗證和脫敏處理
- 使用user/group標(biāo)識限制父進(jìn)程和子進(jìn)程的權(quán)限
- 在隔離環(huán)境中運(yùn)行進(jìn)程, 以防止在其他準(zhǔn)備工作失敗時產(chǎn)生不必要的副作用
const { exec } = require('child_process');
...
// 例如, 以一個腳本為例, 它采用兩個參數(shù), 其中一個參數(shù)是未經(jīng)脫敏處理的用戶輸入
exec('"/path/to/test file/someScript.sh" --someOption ' + input);
// -> 想象一下, 如果用戶只是輸入'&& rm -rf --no-preserve-root /'類似的東西, 會發(fā)生什么
// 你會得到一個不想要的結(jié)果
隱藏客戶端的錯誤詳細(xì)信息
您實現(xiàn)自己的錯誤處理邏輯與自定義錯誤對象(被許多人認(rèn)為是最佳做法)。如果這樣做, 請確保不將整個Error對象返回到客戶端, 這可能包含一些敏感的應(yīng)用程序詳細(xì)信息从橘。
否則敏感應(yīng)用程序詳細(xì)信息(如服務(wù)器文件路徑念赶、使用中的第三方模塊和可能被攻擊者利用的應(yīng)用程序的其他內(nèi)部工作流)可能會從stack trace發(fā)現(xiàn)的信息中泄露。避免將機(jī)密信息發(fā)布到NPM倉庫
應(yīng)該采取預(yù)防措施來避免偶然地將機(jī)密信息發(fā)布到npm倉庫的風(fēng)險恰力。 一個.npmignore
文件可以被用作忽略掉特定的文件或目錄, 或者一個在 package.json 中的files
數(shù)組可以起到一個白名單的作用.
一個. npmignore文件
#tests
test
coverage
#build tools
.travis.yml
.jenkins.yml
#environment
.env
.config