Atlas / Skills / utkusen / Sast Fileupload

Sast FileuploadSAFE

skills/utkusen/sast-fileupload

Collection of agent skills to find vulnerabilities inside your web/mobile apps.

Verdict
SAFE
Grade
B
Trust score
89 /100
Version
—
Hosts
—
License
MIT
Stars
1,336
01

Overview

Collection of agent skills to find vulnerabilities inside your web/mobile apps.

Read from source at commit d9c848ebb5f1OBSERVED · 2026-10-13
02

What it tells the agent

The instruction file, verbatim from the audited commit — this is the text the model reads, and the surface the audit's instruction layer examines. Quoted here so you can judge it without cloning anything.

---
name: sast-fileupload
description: >-
  Detect insecure file upload vulnerabilities in a codebase using a three-phase
  approach: discovery (find all upload sites), batched verify (check extension
  bypass and related issues in parallel subagents, 3 sites each), and merge
  (consolidate batch results). Requires sast/architecture.md (run sast-analysis
  first). Outputs findings to sast/fileupload-results.md. Use when asked to find
  file upload, unrestricted upload, or extension bypass bugs.
---

# Insecure File Upload Detection

You are performing a focused security assessment to find insecure file upload vulnerabilities in a codebase. This skill uses a three-phase approach with subagents: **discovery** (find all places where uploaded files are received and stored), **batched verify** (check bypass vectors in parallel batches of up to 3 upload sites each), and **merge** (consolidate batch reports into one results file).

**Prerequisites**: `sast/architecture.md` must exist. Run the analysis skill first if it doesn't.

---

## What is an Insecure File Upload

Insecure file upload occurs when an application accepts files from users without properly validating or restricting what can be uploaded, allowing an attacker to upload executable or malicious files. The most critical outcome is **Remote Code Execution (RCE)**: an attacker uploads a web shell (e.g., a `.php` file) and the server executes it when accessed via a direct URL.

The core pattern: *a user-supplied file reaches a storage location without adequate extension validation, and the stored file is accessible or executable.*

### What Insecure File Upload IS

- Accepting any file type with no extension or content check: `file.save(upload_path)` with no validation
- Content-Type-only validation: checking `Content-Type: image/png` without verifying the actual extension or file content — trivially bypassed by setting the header manually
- Extension blocklist with gaps: `.php` is blocked but `.php3`, `.php4`, `.php5`, `.phtml`, `.phar`, `.shtml` are not
- Case-insensitive bypass: blocking `.php` but allowing `.PHP`, `.Php`, `.pHp`
- Double extension bypass: `shell.php.jpg` — code extracts the last `.jpg` and considers it safe, but the server (Apache) serves it as PHP
- Path traversal in filenames: `../../webroot/shell.php` stored via an unsanitized filename
- Incomplete filename sanitization: only stripping `../` but not encoded variants `%2e%2e%2f`
- Serving uploaded files from a web-executable directory without disabling execution

### What Insecure File Upload is NOT

Do not flag these as file upload vulnerabilities:

- **Stored XSS via SVG**: uploading an SVG with embedded `<script>` that is reflected back — that's XSS, not an upload execution issue
- **SSRF via file content**: uploading an XML or SVG that triggers an outbound request — that's XXE/SSRF, not a file upload execution issue
- **DoS via large files**: missing file size limits — a separate availability issue
- **IDOR on download**: accessing another user's uploaded file without authorization — that's IDOR
- **Secure uploads**: files stored outside the web root, or served through a controlled download endpoint that sets `Content-Disposition: attachment`, or stored in an object storage bucket with no public execution capability

### Patterns That Prevent Insecure File Upload

When you see these patterns together, the code is likely **not vulnerable**:

**1. Allowlist of safe extensions (most important)**
```python
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'pdf'}
ext = filename.rsplit('.', 1)[-1].lower()
if ext not in ALLOWED_EXTENSIONS:
    abort(400)
```

**2. Magic byte / file content validation (defense in depth)**
```python
import magic
mime = magic.from_buffer(file.read(2048), mime=True)
ALLOWED_MIMES = {'image/png', 'image/jpeg', 'image/gif'}
if mime not in ALLOWED_MIMES:
    abort(400)
```

**3. Filename sanitization using a trusted library**
```python
from werkzeug.utils import secure_filename
filename = secure_filename(file.filename)  # strips path separators and dangerous chars
```

**4. Storing uploads outside the web root**
```
/var/uploads/  ← not served by the web server
/var/www/html/ ← web root (do NOT store uploads here)
```

**5. Serving uploads through a controlled endpoint with Content-Disposition**
```python
@app.route('/download/<filename>')
def download(filename):
    return send_from_directory(UPLOAD_FOLDER, filename,
                               as_attachment=True)  # forces download, prevents execution
```

**6. Renaming the file to a server-generated UUID**
```python
import uuid
stored_name = str(uuid.uuid4()) + '.jpg'  # extension is server-controlled, not user-controlled
```

---

## Vulnerable vs. Secure Examples

### Python — Flask

```python
# VULNERABLE: no extension check, file stored in web-accessible directory
@app.route('/upload', methods=['POST'])
def upload():
    f = request.files['file']
    f.save(os.path.join('static/uploads', f.filename))
    return 'uploaded'

# VULNERABLE: content-type only check (trivially bypassed with curl -H)
@app.route('/upload', methods=['POST'])
def upload():
    f = request.files['file']
    if f.content_type not in ['image/png', 'image/jpeg']:
        abort(400)
    f.save(os.path.join('static/uploads', f.filename))
    return 'uploaded'

# VULNERABLE: blocklist — .phtml/.phar/.php5 not covered
BLOCKED = {'.php', '.sh', '.exe'}
@app.route('/upload', methods=['POST'])
def upload():
    f = request.files['file']
    ext = os.path.splitext(f.filename)[1].lower()
    if ext in BLOCKED:
        abort(400)
    f.save(os.path.join('static/uploads', f.filename))
    return 'uploaded'

# SECURE: allowlist + sanitized filename + outside web root
ALLOWED = {'png', 'jpg', 'jpeg', 'gif'}
UPLOAD_FOLDER = '/var/uploads'  # outside web root

@app.route('/upload', methods=['POST'])
def upload():
    f = request.files['file']
    filename = secure_filename(f.filename)
    ext = filename.rsplit('.', 1)[-1].
03

Trust audit

SAFEgrade B · trust 89/100 Nothing in the source contradicts what it says it does. Grade A is reserved for packages that have also passed the behavioural sandbox.

LayerWhat it checksResult
L0Provenance & inventoryPASS
L1Static analysis of the codeNA
L2Instruction surface (what it tells the agent)PASS
L3Class-specific surfacePASS
L4Behavioural (sandbox)SKIPPED

What the source does

Filesystem
none-observed
Network
none-observed
Shell
none-observed
Dependencies
pinned
Secrets in source
none-found

Findings (0)

No findings outside the package's declared scope.

Gates applied: no_behavioural_pass.

Audited 2026-10-13 · audit v0.4.1 · source sha d9c848ebb5f1full audit observations/trust-audit/skill/utkusen__sast-fileupload.json · Report an issue / request a re-scan
04

Audit history

Every audit this skill has had.

DateSourceVerdictGradeScoreChange
2026-10-13d9c848ebb5f1SAFEB89first audit
05

Questions

What does the Sast Fileupload skill do?

Collection of agent skills to find vulnerabilities inside your web/mobile apps.

Is Sast Fileupload safe to install?

The audit found nothing in the source that contradicts what it says it does, and graded it B (89/100). Grade A is held back for packages that have also passed a sandboxed behavioural run, which is why a clean skill reads B.

What can Sast Fileupload access on my machine?

The audit observed no filesystem, network or shell use at all in its source.

How current is this page?

The grade is for one exact copy of the source (d9c848ebb5f1), read on 2026-10-13. The repository is watched, and a new audit runs when it changes — this is the first audit.

Advertisement