Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a1984e615a | |||
| b157504ca6 | |||
| b131e1b11d | |||
| 2df69ec1c6 |
@@ -35,7 +35,10 @@ the global cap to 60 per trusted client address. Do not enable it on the direct
|
||||
Limits count failures and successes. Registration/resend always give the same success
|
||||
response for existing/unknown emails, and login uses a dummy KDF for unknown accounts.
|
||||
The rate limit itself can reveal recent activity for a known email; it does not reveal
|
||||
whether an account exists. No password recovery or account deletion is included.
|
||||
whether an account exists. Synchronous SMTP latency can still expose a timing side channel;
|
||||
a durable asynchronous mail queue would be needed to fully decouple delivery timing.
|
||||
Logout remains available when login/mail limits are exhausted. No password recovery or
|
||||
account deletion is included.
|
||||
|
||||
## Verification
|
||||
|
||||
|
||||
@@ -14,10 +14,11 @@ export async function POST(request: Request,context: Context) {
|
||||
const {action} = await context.params;
|
||||
if (!['register','resend','verify','login','logout'].includes(action)) return json({error:'不存在'},404);
|
||||
try {
|
||||
// Revocation remains available even when login or mail limits are exhausted.
|
||||
if (action === 'logout') { await logoutAccount(request); const r=json({ok:true});r.headers.set('Set-Cookie',accountCookie('',true));return r; }
|
||||
// Use a global cap by default: forwarding headers are untrusted unless the deployment explicitly enables a trusted ingress.
|
||||
const ip = process.env.ACCOUNT_TRUST_PROXY === 'true' ? request.headers.get('x-forwarded-for')?.split(',')[0].trim() || 'unknown' : 'global';
|
||||
if (!await rateLimit('request:'+ip,process.env.ACCOUNT_TRUST_PROXY === 'true' ? 60 : 300)) return json({error:'请求过于频繁,请稍后再试'},429);
|
||||
if (action === 'logout') { await logoutAccount(request); const r=json({ok:true});r.headers.set('Set-Cookie',accountCookie('',true));return r; }
|
||||
if (Number(request.headers.get('content-length') || 0)>4096) return json({error:'请求过大'},413);
|
||||
const text=await request.text(); if(text.length>4096)return json({error:'请求过大'},413);
|
||||
let body;try{body=JSON.parse(text);}catch{return json({error:'请求格式错误'},400);}
|
||||
|
||||
@@ -34,14 +34,18 @@ test('proxy protects APIs, data routes, static content and Studio; fails closed'
|
||||
const temp=mkdtempSync(join(tmpdir(),'prompt-auth-')); const old=process.env.AUTH_CONFIG_FILE;
|
||||
try{
|
||||
const config=newAuthConfig(); process.env.AUTH_CONFIG_FILE=join(temp,'config.json'); writeFileSync(process.env.AUTH_CONFIG_FILE,JSON.stringify(config));
|
||||
for(const path of ['/','/library/import','/skills/example.png','/_next/data/build/index.json','/studio']) assert.equal(proxy(new NextRequest('http://wiki.test'+path)).status,307,path);
|
||||
for(const path of ['/library/import','/skills/example.png','/_next/data/build/index.json','/studio']) assert.equal(proxy(new NextRequest('http://wiki.test'+path)).status,307,path);
|
||||
for(const path of ['/api/upload','/studio/api/export','/api/feed']) assert.equal(proxy(new NextRequest('http://wiki.test'+path)).status,401,path);
|
||||
for(const path of ['/login','/brand-mark.svg','/_next/static/chunk.js','/api/health','/api/ready']) assert.equal(proxy(new NextRequest('http://wiki.test'+path)).status,200,path);
|
||||
for(const path of ['/','/guide/welcome','/protocol','/api/guides/welcome','/account','/api/account/session','/login','/brand-mark.svg','/_next/static/chunk.js','/api/health','/api/ready']) assert.equal(proxy(new NextRequest('http://wiki.test'+path)).status,200,path);
|
||||
const headers={cookie:'prompt_session='+signSession(config),host:'wiki.test'};
|
||||
assert.equal(proxy(new NextRequest('http://wiki.test/api/feed',{headers})).status,200);
|
||||
assert.equal(proxy(new NextRequest('http://wiki.test/api/posts',{method:'POST',headers:{...headers,origin:'http://evil.test'}})).status,403);
|
||||
assert.equal(proxy(new NextRequest('http://wiki.test/api/posts',{method:'POST',headers:{...headers,origin:'http://wiki.test'}})).status,200);
|
||||
writeFileSync(process.env.AUTH_CONFIG_FILE,'{}'); assert.throws(()=>authConfig()); assert.equal(proxy(new NextRequest('http://wiki.test/')).status,503);
|
||||
assert.equal(proxy(new NextRequest('http://wiki.test/api/posts',{method:'POST',headers:{cookie:'wiki_account='+ 'a'.repeat(64)}})).status,401);
|
||||
assert.equal(proxy(new NextRequest('http://wiki.test/guide/welcome',{method:'POST'})).status,401);
|
||||
writeFileSync(process.env.AUTH_CONFIG_FILE,'{}'); assert.throws(()=>authConfig()); assert.equal(proxy(new NextRequest('http://wiki.test/api/posts')).status,503);
|
||||
delete process.env.AUTH_CONFIG_FILE;
|
||||
assert.equal(proxy(new NextRequest('http://wiki.test/api/posts')).status,503,'Missing admin configuration fails closed');
|
||||
}finally{if(old===undefined)delete process.env.AUTH_CONFIG_FILE;else process.env.AUTH_CONFIG_FILE=old;rmSync(temp,{recursive:true,force:true});}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user