Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions src/ui/src/builder/BuilderHeaderMoreDropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,28 @@
v-model="importConfirmCheckbox"
label="Yes, I want to replace the current agent with the imported file"
/>
<div
v-if="importErrorSummary"
class="BuilderHeaderMoreDropdown__importError"
>
<p class="BuilderHeaderMoreDropdown__importError__header">
Failed to import the agent
</p>

<p class="BuilderHeaderMoreDropdown__importError__summary">
{{ importErrorSummary }}
</p>

<details
v-if="importErrorDetails"
class="BuilderHeaderMoreDropdown__importError__details"
>
<summary>Show details</summary>
<pre class="BuilderHeaderMoreDropdown__importError__trace">{{
importErrorDetails
}}</pre>
</details>
</div>
</WdsModal>
</template>

Expand Down Expand Up @@ -60,6 +82,8 @@ const toasts = useToasts();
const importInProgress = ref(false);
const importConfirmShown = ref(false);
const importConfirmCheckbox = ref(false);
const importErrorSummary = ref("");
const importErrorDetails = ref("");

const importModalActions = computed<ModalAction[]>(() => [
{
Expand All @@ -68,6 +92,8 @@ const importModalActions = computed<ModalAction[]>(() => [
fn: () => {
importConfirmCheckbox.value = false;
importConfirmShown.value = false;
importErrorSummary.value = "";
importErrorDetails.value = "";
},
},
{
Expand Down Expand Up @@ -105,6 +131,8 @@ async function importModalConfirm() {
if (!file) return;

importInProgress.value = true;
importErrorSummary.value = "";
importErrorDetails.value = "";

try {
// Prepare form data
Expand All @@ -117,6 +145,9 @@ async function importModalConfirm() {
body: formData,
});
if (!response.ok) {
const parsed = await response.json();
importErrorSummary.value = parsed?.detail?.summary;
importErrorDetails.value = parsed?.detail?.details;
throw new Error("Failed to connect to import API");
}
importConfirmShown.value = false;
Expand Down Expand Up @@ -162,4 +193,50 @@ async function onSelect(key: string) {
.BuilderHeaderMoreDropdown:deep(.SharedMoreDropdown__dropdown) {
min-width: 220px;
}

.BuilderHeaderMoreDropdown__importError {
margin-top: 16px;
padding: 12px 14px;
border-radius: 8px;
background: rgba(255, 149, 0, 0.08);
border: 1px solid rgba(255, 149, 0, 0.35);
grid-column: 2 / 4;
}

.BuilderHeaderMoreDropdown__importError__header {
font-weight: 600;
color: var(--wdsColorOrange4);
font-size: 14px;
}

.BuilderHeaderMoreDropdown__importError__summary {
margin-top: 6px;
font-size: 14px;
}

.BuilderHeaderMoreDropdown__importError__details {
margin-top: 10px;
}

.BuilderHeaderMoreDropdown__importError__details summary {
cursor: pointer;
font-size: 12px;
user-select: none;
}

.BuilderHeaderMoreDropdown__importError__details summary:hover {
text-decoration: underline;
}

.BuilderHeaderMoreDropdown__importError__trace {
margin-top: 8px;
max-height: 220px;
overflow: auto;
padding: 10px;
border-radius: 6px;
background: rgba(0, 0, 0, 0.05);
font-size: 12px;
line-height: 1.4;
white-space: pre;
}
</style>
3 changes: 3 additions & 0 deletions src/writer/app_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -1207,6 +1207,9 @@ async def import_zip(self, zip_path: str):
if not os.path.isdir(wf_dir_path):
raise ValueError(".wf directory not found alongside main.py in the archive.")

# Parse files to ensure there are no errors
wf_project.read_files(main_py_dir)

# Passed all checks; replace current app contents

logging.info("Copying app at %s", main_py_dir)
Expand Down
10 changes: 5 additions & 5 deletions src/writer/serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,9 +263,9 @@ async def export_zip():
@app.post("/api/import")
async def import_zip(file: UploadFile = File(...)):
if serve_mode != "edit":
raise HTTPException(status_code=403, detail="Invalid mode.")
raise HTTPException(status_code=403, detail={"summary": "Invalid mode. Expected 'edit'"})
if not file.filename or not file.filename.endswith(".zip"):
raise HTTPException(status_code=400, detail="Only .zip files are supported.")
raise HTTPException(status_code=400, detail={"summary": "Only .zip files are supported."})

MAX_FILE_SIZE = 200 * 1024 * 1024

Expand All @@ -278,13 +278,13 @@ async def import_zip(file: UploadFile = File(...)):
if size > MAX_FILE_SIZE:
tmp.close()
os.unlink(tmp.name)
raise HTTPException(status_code=413, detail=f"File too large. Max file size: {MAX_FILE_SIZE}")
raise HTTPException(status_code=413, detail={"summary": f"File too large. Max file size: {MAX_FILE_SIZE}"})
tmp.write(chunk)
tmp_path = tmp.name
await app_runner.import_zip(tmp_path)
os.remove(tmp_path)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid upload.")
except ValueError as e:
raise HTTPException(status_code=400, detail={"summary": "Invalid archive contents", "details": traceback.format_exc()}) from e
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The full stack trace is included in the HTTP response which can potentially expose sensitive system details.

Risk
Exposing stack traces in API responses can:

  • Aid attackers in understanding internal code structure
  • Reveal sensitive configuration or environment details
  • Increase the effectiveness of targeted attacks and vulnerability exploitation

References:
CWE-532: Insertion of Sensitive Information into Log File
[OWASP Error Handling Guidelines]

Recommendation:

  • Do not include stack traces or raw exception details in client-facing API responses.
🔺 Vulnerability (Error)

Image of Francisco P Francisco P


@app.post("/api/autogen")
async def autogen(requestBody: AutogenRequestBody, request: Request):
Expand Down