evernote-security-basics
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseEvernote Security Basics
Evernote 安全基础
Overview
概述
Security best practices for Evernote API integrations, covering credential management, OAuth implementation, data protection, and secure coding patterns.
Evernote API集成的安全最佳实践,涵盖凭证管理、OAuth实现、数据保护和安全编码模式。
Prerequisites
前置条件
- Evernote SDK setup
- Understanding of OAuth 1.0a
- Basic security concepts
- Evernote SDK 已配置
- 了解 OAuth 1.0a
- 掌握基础安全概念
Credential Security
凭证安全
Step 1: Environment Variables
步骤1:环境变量
bash
undefinedbash
undefined.env (NEVER commit this file)
.env (绝对不要提交此文件)
EVERNOTE_CONSUMER_KEY=your-consumer-key
EVERNOTE_CONSUMER_SECRET=your-consumer-secret
EVERNOTE_SANDBOX=true
EVERNOTE_CONSUMER_KEY=your-consumer-key
EVERNOTE_CONSUMER_SECRET=your-consumer-secret
EVERNOTE_SANDBOX=true
.gitignore
.gitignore
.env
.env.local
.env.*.local
*.pem
*.key
```javascript
// config/evernote.js
require('dotenv').config();
const config = {
consumerKey: process.env.EVERNOTE_CONSUMER_KEY,
consumerSecret: process.env.EVERNOTE_CONSUMER_SECRET,
sandbox: process.env.EVERNOTE_SANDBOX === 'true'
};
// Validate required credentials at startup
const required = ['consumerKey', 'consumerSecret'];
for (const key of required) {
if (!config[key]) {
throw new Error(`Missing required config: ${key}`);
}
}
module.exports = Object.freeze(config);.env
.env.local
.env.*.local
*.pem
*.key
```javascript
// config/evernote.js
require('dotenv').config();
const config = {
consumerKey: process.env.EVERNOTE_CONSUMER_KEY,
consumerSecret: process.env.EVERNOTE_CONSUMER_SECRET,
sandbox: process.env.EVERNOTE_SANDBOX === 'true'
};
// 启动时验证必填凭证
const required = ['consumerKey', 'consumerSecret'];
for (const key of required) {
if (!config[key]) {
throw new Error(`Missing required config: ${key}`);
}
}
module.exports = Object.freeze(config);Step 2: Secret Manager Integration
步骤2:密钥管理器集成
javascript
// For production, use a secret manager
// AWS Secrets Manager
const { SecretsManagerClient, GetSecretValueCommand } = require('@aws-sdk/client-secrets-manager');
async function getEvernoteSecrets() {
const client = new SecretsManagerClient({ region: 'us-east-1' });
const response = await client.send(
new GetSecretValueCommand({
SecretId: 'evernote/api-credentials'
})
);
return JSON.parse(response.SecretString);
}
// Google Secret Manager
const { SecretManagerServiceClient } = require('@google-cloud/secret-manager');
async function getSecretFromGCP(secretName) {
const client = new SecretManagerServiceClient();
const [version] = await client.accessSecretVersion({
name: `projects/my-project/secrets/${secretName}/versions/latest`
});
return version.payload.data.toString('utf8');
}javascript
// 生产环境请使用密钥管理器
// AWS Secrets Manager
const { SecretsManagerClient, GetSecretValueCommand } = require('@aws-sdk/client-secrets-manager');
async function getEvernoteSecrets() {
const client = new SecretsManagerClient({ region: 'us-east-1' });
const response = await client.send(
new GetSecretValueCommand({
SecretId: 'evernote/api-credentials'
})
);
return JSON.parse(response.SecretString);
}
// Google Secret Manager
const { SecretManagerServiceClient } = require('@google-cloud/secret-manager');
async function getSecretFromGCP(secretName) {
const client = new SecretManagerServiceClient();
const [version] = await client.accessSecretVersion({
name: `projects/my-project/secrets/${secretName}/versions/latest`
});
return version.payload.data.toString('utf8');
}Step 3: Secure Token Storage
步骤3:安全令牌存储
javascript
// services/token-store.js
/**
* Never store tokens in:
* - Plain text files
* - Client-side storage (localStorage, cookies without httpOnly)
* - Source code
* - Log files
*/
class SecureTokenStore {
constructor(encryptionKey) {
this.crypto = require('crypto');
this.algorithm = 'aes-256-gcm';
this.key = this.deriveKey(encryptionKey);
}
deriveKey(password) {
return this.crypto.scryptSync(password, 'salt', 32);
}
encrypt(token) {
const iv = this.crypto.randomBytes(16);
const cipher = this.crypto.createCipheriv(this.algorithm, this.key, iv);
let encrypted = cipher.update(token, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return {
encrypted,
iv: iv.toString('hex'),
authTag: authTag.toString('hex')
};
}
decrypt(encryptedData) {
const decipher = this.crypto.createDecipheriv(
this.algorithm,
this.key,
Buffer.from(encryptedData.iv, 'hex')
);
decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'hex'));
let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
}
module.exports = SecureTokenStore;javascript
// services/token-store.js
/**
* 绝对不要在以下位置存储令牌:
* - 纯文本文件
* - 客户端存储(localStorage、未设置httpOnly的cookie)
* - 源代码
* - 日志文件
*/
class SecureTokenStore {
constructor(encryptionKey) {
this.crypto = require('crypto');
this.algorithm = 'aes-256-gcm';
this.key = this.deriveKey(encryptionKey);
}
deriveKey(password) {
return this.crypto.scryptSync(password, 'salt', 32);
}
encrypt(token) {
const iv = this.crypto.randomBytes(16);
const cipher = this.crypto.createCipheriv(this.algorithm, this.key, iv);
let encrypted = cipher.update(token, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return {
encrypted,
iv: iv.toString('hex'),
authTag: authTag.toString('hex')
};
}
decrypt(encryptedData) {
const decipher = this.crypto.createDecipheriv(
this.algorithm,
this.key,
Buffer.from(encryptedData.iv, 'hex')
);
decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'hex'));
let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
}
module.exports = SecureTokenStore;OAuth Security
OAuth 安全
Step 4: Secure OAuth Implementation
步骤4:安全OAuth实现
javascript
// routes/oauth.js
const express = require('express');
const Evernote = require('evernote');
const crypto = require('crypto');
const router = express.Router();
// Generate CSRF token
function generateCSRFToken() {
return crypto.randomBytes(32).toString('hex');
}
// Initiate OAuth with CSRF protection
router.get('/auth/evernote', (req, res) => {
// Generate and store CSRF token
const csrfToken = generateCSRFToken();
req.session.csrfToken = csrfToken;
const client = new Evernote.Client({
consumerKey: process.env.EVERNOTE_CONSUMER_KEY,
consumerSecret: process.env.EVERNOTE_CONSUMER_SECRET,
sandbox: process.env.EVERNOTE_SANDBOX === 'true'
});
// Include CSRF token in callback URL
const callbackUrl = `${process.env.APP_URL}/auth/evernote/callback?csrf=${csrfToken}`;
client.getRequestToken(callbackUrl, (error, oauthToken, oauthTokenSecret) => {
if (error) {
console.error('OAuth request token error:', error);
return res.status(500).json({ error: 'Failed to initiate authentication' });
}
// Store tokens securely in session
req.session.oauthToken = oauthToken;
req.session.oauthTokenSecret = oauthTokenSecret;
req.session.oauthTimestamp = Date.now();
res.redirect(client.getAuthorizeUrl(oauthToken));
});
});
// Handle OAuth callback with validation
router.get('/auth/evernote/callback', (req, res) => {
// Validate CSRF token
if (req.query.csrf !== req.session.csrfToken) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
// Check OAuth request timeout (5 minutes)
const timeout = 5 * 60 * 1000;
if (Date.now() - req.session.oauthTimestamp > timeout) {
return res.status(400).json({ error: 'OAuth request expired' });
}
// Validate required parameters
if (!req.query.oauth_verifier) {
return res.status(400).json({ error: 'Missing oauth_verifier' });
}
const client = new Evernote.Client({
consumerKey: process.env.EVERNOTE_CONSUMER_KEY,
consumerSecret: process.env.EVERNOTE_CONSUMER_SECRET,
sandbox: process.env.EVERNOTE_SANDBOX === 'true'
});
client.getAccessToken(
req.session.oauthToken,
req.session.oauthTokenSecret,
req.query.oauth_verifier,
(error, accessToken, accessTokenSecret, results) => {
if (error) {
console.error('OAuth access token error:', error);
return res.status(500).json({ error: 'Failed to complete authentication' });
}
// Clear OAuth session data
delete req.session.oauthToken;
delete req.session.oauthTokenSecret;
delete req.session.oauthTimestamp;
delete req.session.csrfToken;
// Store access token securely
// DO NOT log the token
req.session.evernoteToken = accessToken;
req.session.evernoteExpires = parseInt(results.edam_expires);
res.redirect('/dashboard');
}
);
});
module.exports = router;javascript
// routes/oauth.js
const express = require('express');
const Evernote = require('evernote');
const crypto = require('crypto');
const router = express.Router();
// 生成CSRF令牌
function generateCSRFToken() {
return crypto.randomBytes(32).toString('hex');
}
// 启动带有CSRF保护的OAuth
router.get('/auth/evernote', (req, res) => {
// 生成并存储CSRF令牌
const csrfToken = generateCSRFToken();
req.session.csrfToken = csrfToken;
const client = new Evernote.Client({
consumerKey: process.env.EVERNOTE_CONSUMER_KEY,
consumerSecret: process.env.EVERNOTE_CONSUMER_SECRET,
sandbox: process.env.EVERNOTE_SANDBOX === 'true'
});
// 在回调URL中包含CSRF令牌
const callbackUrl = `${process.env.APP_URL}/auth/evernote/callback?csrf=${csrfToken}`;
client.getRequestToken(callbackUrl, (error, oauthToken, oauthTokenSecret) => {
if (error) {
console.error('OAuth request token error:', error);
return res.status(500).json({ error: 'Failed to initiate authentication' });
}
// 在会话中安全存储令牌
req.session.oauthToken = oauthToken;
req.session.oauthTokenSecret = oauthTokenSecret;
req.session.oauthTimestamp = Date.now();
res.redirect(client.getAuthorizeUrl(oauthToken));
});
});
// 验证并处理OAuth回调
router.get('/auth/evernote/callback', (req, res) => {
// 验证CSRF令牌
if (req.query.csrf !== req.session.csrfToken) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
// 检查OAuth请求超时(5分钟)
const timeout = 5 * 60 * 1000;
if (Date.now() - req.session.oauthTimestamp > timeout) {
return res.status(400).json({ error: 'OAuth request expired' });
}
// 验证必填参数
if (!req.query.oauth_verifier) {
return res.status(400).json({ error: 'Missing oauth_verifier' });
}
const client = new Evernote.Client({
consumerKey: process.env.EVERNOTE_CONSUMER_KEY,
consumerSecret: process.env.EVERNOTE_CONSUMER_SECRET,
sandbox: process.env.EVERNOTE_SANDBOX === 'true'
});
client.getAccessToken(
req.session.oauthToken,
req.session.oauthTokenSecret,
req.query.oauth_verifier,
(error, accessToken, accessTokenSecret, results) => {
if (error) {
console.error('OAuth access token error:', error);
return res.status(500).json({ error: 'Failed to complete authentication' });
}
// 清除OAuth会话数据
delete req.session.oauthToken;
delete req.session.oauthTokenSecret;
delete req.session.oauthTimestamp;
delete req.session.csrfToken;
// 安全存储访问令牌
// 请勿记录令牌
req.session.evernoteToken = accessToken;
req.session.evernoteExpires = parseInt(results.edam_expires);
res.redirect('/dashboard');
}
);
});
module.exports = router;Step 5: Session Security
步骤5:会话安全
javascript
// config/session.js
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');
const redisClient = createClient({
url: process.env.REDIS_URL
});
redisClient.connect().catch(console.error);
module.exports = session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
name: 'sessionId', // Don't use default 'connect.sid'
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production', // HTTPS only in production
httpOnly: true, // Prevent XSS access
sameSite: 'lax', // CSRF protection
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
});javascript
// config/session.js
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');
const redisClient = createClient({
url: process.env.REDIS_URL
});
redisClient.connect().catch(console.error);
module.exports = session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
name: 'sessionId', // 不要使用默认的'connect.sid'
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production', // 生产环境仅使用HTTPS
httpOnly: true, // 防止XSS访问
sameSite: 'lax', // CSRF保护
maxAge: 24 * 60 * 60 * 1000 // 24小时
}
});Input Validation
输入验证
Step 6: Validate User Input
步骤6:验证用户输入
javascript
// utils/validators.js
const validator = {
/**
* Validate note title
*/
noteTitle(title) {
if (!title || typeof title !== 'string') {
throw new Error('Note title is required');
}
// Max 255 characters
if (title.length > 255) {
throw new Error('Note title must be 255 characters or less');
}
// No control characters
if (/[\x00-\x1f]/.test(title)) {
throw new Error('Note title contains invalid characters');
}
return title.trim();
},
/**
* Validate notebook name
*/
notebookName(name) {
if (!name || typeof name !== 'string') {
throw new Error('Notebook name is required');
}
if (name.length > 100) {
throw new Error('Notebook name must be 100 characters or less');
}
return name.trim();
},
/**
* Validate tag name
*/
tagName(name) {
if (!name || typeof name !== 'string') {
throw new Error('Tag name is required');
}
if (name.length > 100) {
throw new Error('Tag name must be 100 characters or less');
}
// Tags cannot start with #
if (name.startsWith('#')) {
throw new Error('Tag name cannot start with #');
}
return name.trim();
},
/**
* Validate GUID format
*/
guid(guid) {
if (!guid || typeof guid !== 'string') {
throw new Error('GUID is required');
}
// Evernote GUIDs are typically UUID format
const guidPattern = /^[a-f0-9-]{36}$/i;
if (!guidPattern.test(guid)) {
throw new Error('Invalid GUID format');
}
return guid;
},
/**
* Sanitize ENML content
*/
enmlContent(content) {
if (!content || typeof content !== 'string') {
throw new Error('Content is required');
}
// Remove potentially dangerous elements
let sanitized = content
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
.replace(/<iframe[^>]*>[\s\S]*?<\/iframe>/gi, '')
.replace(/<object[^>]*>[\s\S]*?<\/object>/gi, '')
.replace(/<embed[^>]*>/gi, '')
.replace(/<form[^>]*>[\s\S]*?<\/form>/gi, '');
// Remove event handlers
sanitized = sanitized.replace(/\s+on\w+\s*=\s*["'][^"']*["']/gi, '');
// Remove javascript: URLs
sanitized = sanitized.replace(/javascript:/gi, '');
return sanitized;
}
};
module.exports = validator;javascript
// utils/validators.js
const validator = {
/**
* Validate note title
*/
noteTitle(title) {
if (!title || typeof title !== 'string') {
throw new Error('Note title is required');
}
// Max 255 characters
if (title.length > 255) {
throw new Error('Note title must be 255 characters or less');
}
// No control characters
if (/[\x00-\x1f]/.test(title)) {
throw new Error('Note title contains invalid characters');
}
return title.trim();
},
/**
* Validate notebook name
*/
notebookName(name) {
if (!name || typeof name !== 'string') {
throw new Error('Notebook name is required');
}
if (name.length > 100) {
throw new Error('Notebook name must be 100 characters or less');
}
return name.trim();
},
/**
* Validate tag name
*/
tagName(name) {
if (!name || typeof name !== 'string') {
throw new Error('Tag name is required');
}
if (name.length > 100) {
throw new Error('Tag name must be 100 characters or less');
}
// Tags cannot start with #
if (name.startsWith('#')) {
throw new Error('Tag name cannot start with #');
}
return name.trim();
},
/**
* Validate GUID format
*/
guid(guid) {
if (!guid || typeof guid !== 'string') {
throw new Error('GUID is required');
}
// Evernote GUIDs are typically UUID format
const guidPattern = /^[a-f0-9-]{36}$/i;
if (!guidPattern.test(guid)) {
throw new Error('Invalid GUID format');
}
return guid;
},
/**
* Sanitize ENML content
*/
enmlContent(content) {
if (!content || typeof content !== 'string') {
throw new Error('Content is required');
}
// Remove potentially dangerous elements
let sanitized = content
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
.replace(/<iframe[^>]*>[\s\S]*?<\/iframe>/gi, '')
.replace(/<object[^>]*>[\s\S]*?<\/object>/gi, '')
.replace(/<embed[^>]*>/gi, '')
.replace(/<form[^>]*>[\s\S]*?<\/form>/gi, '');
// Remove event handlers
sanitized = sanitized.replace(/\s+on\w+\s*=\s*["'][^"']*["']/gi, '');
// Remove javascript: URLs
sanitized = sanitized.replace(/javascript:/gi, '');
return sanitized;
}
};
module.exports = validator;Step 7: Secure Logging
步骤7:安全日志
javascript
// utils/secure-logger.js
class SecureLogger {
constructor() {
this.sensitivePatterns = [
/S=s\d+:U=[^:]+:[^:]+:[a-f0-9]+/gi, // Evernote tokens
/oauth_token=[^&]+/gi,
/oauth_token_secret=[^&]+/gi,
/consumer_secret=[^&]+/gi,
/password=[^&]+/gi,
/api_key=[^&]+/gi
];
}
sanitize(data) {
if (typeof data === 'string') {
let sanitized = data;
for (const pattern of this.sensitivePatterns) {
sanitized = sanitized.replace(pattern, '[REDACTED]');
}
return sanitized;
}
if (typeof data === 'object' && data !== null) {
const sanitized = Array.isArray(data) ? [] : {};
for (const [key, value] of Object.entries(data)) {
const lowerKey = key.toLowerCase();
if (lowerKey.includes('token') ||
lowerKey.includes('secret') ||
lowerKey.includes('password') ||
lowerKey.includes('key')) {
sanitized[key] = '[REDACTED]';
} else {
sanitized[key] = this.sanitize(value);
}
}
return sanitized;
}
return data;
}
log(level, message, data = null) {
const entry = {
timestamp: new Date().toISOString(),
level,
message,
data: data ? this.sanitize(data) : undefined
};
console[level === 'error' ? 'error' : 'log'](JSON.stringify(entry));
}
info(message, data) {
this.log('info', message, data);
}
error(message, data) {
this.log('error', message, data);
}
warn(message, data) {
this.log('warn', message, data);
}
}
module.exports = new SecureLogger();javascript
// utils/secure-logger.js
class SecureLogger {
constructor() {
this.sensitivePatterns = [
/S=s\d+:U=[^:]+:[^:]+:[a-f0-9]+/gi, // Evernote tokens
/oauth_token=[^&]+/gi,
/oauth_token_secret=[^&]+/gi,
/consumer_secret=[^&]+/gi,
/password=[^&]+/gi,
/api_key=[^&]+/gi
];
}
sanitize(data) {
if (typeof data === 'string') {
let sanitized = data;
for (const pattern of this.sensitivePatterns) {
sanitized = sanitized.replace(pattern, '[REDACTED]');
}
return sanitized;
}
if (typeof data === 'object' && data !== null) {
const sanitized = Array.isArray(data) ? [] : {};
for (const [key, value] of Object.entries(data)) {
const lowerKey = key.toLowerCase();
if (lowerKey.includes('token') ||
lowerKey.includes('secret') ||
lowerKey.includes('password') ||
lowerKey.includes('key')) {
sanitized[key] = '[REDACTED]';
} else {
sanitized[key] = this.sanitize(value);
}
}
return sanitized;
}
return data;
}
log(level, message, data = null) {
const entry = {
timestamp: new Date().toISOString(),
level,
message,
data: data ? this.sanitize(data) : undefined
};
console[level === 'error' ? 'error' : 'log'](JSON.stringify(entry));
}
info(message, data) {
this.log('info', message, data);
}
error(message, data) {
this.log('error', message, data);
}
warn(message, data) {
this.log('warn', message, data);
}
}
module.exports = new SecureLogger();Token Lifecycle
令牌生命周期
Step 8: Token Expiration Handling
步骤8:令牌过期处理
javascript
// services/token-manager.js
class TokenManager {
constructor(options = {}) {
this.refreshThresholdDays = options.refreshThresholdDays || 30;
}
/**
* Check if token needs refresh (approaching expiration)
*/
needsRefresh(expirationTimestamp) {
const now = Date.now();
const expiresAt = expirationTimestamp;
const thresholdMs = this.refreshThresholdDays * 24 * 60 * 60 * 1000;
return (expiresAt - now) < thresholdMs;
}
/**
* Check if token is expired
*/
isExpired(expirationTimestamp) {
return Date.now() > expirationTimestamp;
}
/**
* Calculate days until expiration
*/
daysUntilExpiration(expirationTimestamp) {
const ms = expirationTimestamp - Date.now();
return Math.floor(ms / (24 * 60 * 60 * 1000));
}
/**
* Token status summary
*/
getStatus(expirationTimestamp) {
const daysLeft = this.daysUntilExpiration(expirationTimestamp);
return {
expiresAt: new Date(expirationTimestamp),
daysUntilExpiration: daysLeft,
isExpired: this.isExpired(expirationTimestamp),
needsRefresh: this.needsRefresh(expirationTimestamp),
status: this.isExpired(expirationTimestamp) ? 'EXPIRED' :
daysLeft < 7 ? 'CRITICAL' :
daysLeft < 30 ? 'WARNING' : 'OK'
};
}
}
module.exports = TokenManager;javascript
// services/token-manager.js
class TokenManager {
constructor(options = {}) {
this.refreshThresholdDays = options.refreshThresholdDays || 30;
}
/**
* Check if token needs refresh (approaching expiration)
*/
needsRefresh(expirationTimestamp) {
const now = Date.now();
const expiresAt = expirationTimestamp;
const thresholdMs = this.refreshThresholdDays * 24 * 60 * 60 * 1000;
return (expiresAt - now) < thresholdMs;
}
/**
* Check if token is expired
*/
isExpired(expirationTimestamp) {
return Date.now() > expirationTimestamp;
}
/**
* Calculate days until expiration
*/
daysUntilExpiration(expirationTimestamp) {
const ms = expirationTimestamp - Date.now();
return Math.floor(ms / (24 * 60 * 60 * 1000));
}
/**
* Token status summary
*/
getStatus(expirationTimestamp) {
const daysLeft = this.daysUntilExpiration(expirationTimestamp);
return {
expiresAt: new Date(expirationTimestamp),
daysUntilExpiration: daysLeft,
isExpired: this.isExpired(expirationTimestamp),
needsRefresh: this.needsRefresh(expirationTimestamp),
status: this.isExpired(expirationTimestamp) ? 'EXPIRED' :
daysLeft < 7 ? 'CRITICAL' :
daysLeft < 30 ? 'WARNING' : 'OK'
};
}
}
module.exports = TokenManager;Security Checklist
安全检查清单
markdown
undefinedmarkdown
undefinedPre-Production Security Checklist
预生产环境安全检查清单
Credentials
凭证
- API keys stored in environment variables or secret manager
- No credentials in source code
- .env files in .gitignore
- Different credentials for dev/staging/production
- API密钥存储在环境变量或密钥管理器中
- 源代码中无凭证信息
- .env文件已添加到.gitignore
- 开发/预发布/生产环境使用不同凭证
OAuth
OAuth
- CSRF protection implemented
- OAuth state parameter validated
- Request token timeout enforced
- Secure session storage (Redis, database)
- 已实现CSRF保护
- 已验证OAuth状态参数
- 已强制执行请求令牌超时
- 使用安全会话存储(Redis、数据库)
Sessions
会话
- HttpOnly cookies enabled
- Secure flag enabled in production
- SameSite attribute set
- Session timeout configured
- 已启用HttpOnly cookie
- 生产环境已启用Secure标志
- 已设置SameSite属性
- 已配置会话超时
Data Protection
数据保护
- Tokens encrypted at rest
- Sensitive data not logged
- Input validation on all user data
- ENML content sanitized
- 令牌在静态存储时已加密
- 未记录敏感数据
- 对所有用户数据执行输入验证
- 已清理ENML内容
Transport
传输
- HTTPS enforced in production
- TLS 1.2+ required
- Certificate validation enabled
- 生产环境强制使用HTTPS
- 要求使用TLS 1.2+
- 已启用证书验证
Error Handling
错误处理
- No sensitive data in error messages
- Generic error messages to users
- Detailed errors only in secure logs
undefined- 错误消息中无敏感数据
- 向用户返回通用错误消息
- 详细错误仅记录在安全日志中
undefinedOutput
输出成果
- Secure credential management
- CSRF-protected OAuth flow
- Encrypted token storage
- Input validation utilities
- Secure logging with data redaction
- Token lifecycle management
- 安全凭证管理方案
- 带CSRF保护的OAuth流程
- 加密令牌存储
- 输入验证工具
- 带数据脱敏的安全日志
- 令牌生命周期管理
Resources
参考资源
Next Steps
下一步
For production deployment checklist, see .
evernote-prod-checklist生产环境部署检查清单,请查看。
evernote-prod-checklist