feat(upload): 支持 Agent 本地图片安全上传
This commit is contained in:
@@ -83,6 +83,11 @@ try {
|
||||
const blockedPrivateStorage=await request('/api/management/storage-configs',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({region:'ap-guangzhou',bucket:'blocked-private-1234567890',public_base_url:'http://127.0.0.1:8080',cdn_domain:'',path_prefix:'delivery-desk',secret_id:'test-secret-id',secret_key:'test-secret-key'})},adminCookie);
|
||||
expectStatus(blockedPrivateStorage.response.status,400,'拒绝私有地址作为对象存储访问域名',blockedPrivateStorage.body);
|
||||
await database.insertId("INSERT INTO storage_configs (region,bucket,public_base_url,cdn_domain,path_prefix,secret_id_encrypted,secret_key_encrypted,status,test_status,created_by) VALUES (?,?,?,?,?,?,?,?,?,?)", ['ap-guangzhou','runtime-test-1234567890','https://runtime-test-1234567890.cos.ap-guangzhou.myqcloud.com','https://cdn.example.com','delivery-desk',encryptSecret('test-secret-id'),encryptSecret('test-secret-key'),'active','passed',1]);
|
||||
const invalidMultipart=new FormData();
|
||||
invalidMultipart.append('title','伪造图片内容');
|
||||
invalidMultipart.append('images',new Blob(['not-a-real-image'],{type:'image/png'}),'spoofed.png');
|
||||
const invalidUpload=await request(`/api/projects/${projectId}/works`,{method:'POST',body:invalidMultipart},adminCookie);
|
||||
expectStatus(invalidUpload.response.status,422,'拒绝仅伪造 MIME 的上传文件',invalidUpload.body);
|
||||
const privateImageUrls=['http://localhost/private.jpg','http://127.0.0.1/private.jpg','http://10.0.0.1/private.jpg','http://100.64.0.1/private.jpg','http://169.254.0.1/private.jpg','http://172.16.0.1/private.jpg','http://192.168.1.1/private.jpg','http://[::1]/private.jpg','http://[fd00::1]/private.jpg'];
|
||||
for(const [index,imageUrl] of privateImageUrls.entries()){
|
||||
const blockedPrivateImage=await request(`/api/projects/${projectId}/works`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({title:`禁止私有图片 ${index+1}`,images:[imageUrl]})},adminCookie);
|
||||
@@ -107,7 +112,25 @@ try {
|
||||
if(target==='https://93.184.216.34/source.png')return new Response(Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=','base64'),{status:200,headers:{'Content-Type':'image/png'}});
|
||||
return nativeFetch(input,init);
|
||||
}) as typeof fetch;
|
||||
cosPrototype.putObject=(async(options:unknown)=>{importedStorageKey=String((options as {Key:string}).Key);return{statusCode:200}}) as typeof cosPrototype.putObject;
|
||||
cosPrototype.putObject=(async(options:unknown)=>{
|
||||
const item=options as {Key:string;Body?:NodeJS.ReadableStream};
|
||||
importedStorageKey=String(item.Key);
|
||||
if(item.Body&&typeof item.Body.on==='function'){
|
||||
await new Promise<void>((resolve,reject)=>{item.Body!.on('data',()=>undefined);item.Body!.once('end',resolve);item.Body!.once('error',reject)});
|
||||
}
|
||||
return{statusCode:200};
|
||||
}) as typeof cosPrototype.putObject;
|
||||
const localUpload=new FormData();
|
||||
localUpload.append('externalId','runtime-local-file');
|
||||
localUpload.append('title','本地文件上传作品');
|
||||
localUpload.append('tags',JSON.stringify(['#multipart']));
|
||||
const validPng=Uint8Array.from(atob('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII='),(character)=>character.charCodeAt(0));
|
||||
localUpload.append('images',new Blob([validPng],{type:'image/png'}),'generated.png');
|
||||
const localWork=await request(`/api/projects/${otherProjectId}/works`,{method:'POST',body:localUpload},adminCookie);
|
||||
expectStatus(localWork.response.status,201,'multipart 本地图片上传 COS',localWork.body);
|
||||
const localDetail=await request(`/api/works/${Number((localWork.body as {id:number}).id)}`,{},adminCookie);
|
||||
const localImage=(localDetail.body as {images:Array<{url:string;width:number;height:number;storage_provider:string;storage_key:string}>}).images[0];
|
||||
if(!importedStorageKey.startsWith('delivery-desk/originals/')||localImage.url!==`https://cdn.example.com/${importedStorageKey}`||localImage.storage_provider!=='tencent_cos'||localImage.width!==1||localImage.height!==1)throw new Error('multipart 本地图片没有校验并写入当前 COS');
|
||||
const importedWork=await request(`/api/projects/${otherProjectId}/works`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({title:'异源转存作品',images:['https://93.184.216.34/source.png']})},adminCookie);
|
||||
expectStatus(importedWork.response.status,201,'异源图片转存 COS',importedWork.body);
|
||||
const importedDetail=await request(`/api/works/${Number((importedWork.body as {id:number}).id)}`,{},adminCookie);
|
||||
@@ -285,7 +308,7 @@ try {
|
||||
const revoke = await request(`/api/management/api-keys/${keyId}`, { method: 'DELETE' }, adminCookie);
|
||||
expectStatus(revoke.response.status, 204, '吊销平台 API Key');
|
||||
|
||||
process.stdout.write('PostgreSQL 运行时验证通过:账号角色、项目隔离、客户门禁、项目自动状态、完成只读、API Key 发现、externalId 幂等、COS 同源复用、异源转存、SSRF 拦截与单方案轮次\n');
|
||||
process.stdout.write('PostgreSQL 运行时验证通过:账号角色、项目隔离、客户门禁、项目自动状态、完成只读、API Key 发现、externalId 幂等、multipart 本地上传、COS 归一化、SSRF 拦截与单方案轮次\n');
|
||||
} finally {
|
||||
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
||||
await closeDatabase();
|
||||
|
||||
Reference in New Issue
Block a user