sast-fileupload
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).
Install / Use
npx skills add utkusen/sast-skills --skill sast-fileuploadInstalls into whichever agent you are using.
SKILL.md
Installable skill definition
Quality Score
Category
SecuritySupported Platforms
Our assessment of sast-fileupload
sast-fileupload scores 89/100 on our quality scale, 490th of 971 Security skills we index.
Its SKILL.md is 29 KB long, well organised into 35 sections with 16 code examples: a thorough specification that gives an agent plenty to work with.
With 1,321 GitHub stars, it is one of the more widely adopted skills in the catalogue.
Maintenance, license and trust
- The repository was last updated about 6 months ago. That is recent enough to be usable, but agent tooling moves fast, so check the instructions against your agent's current version.
- It is released under the MIT license, a permissive license that allows use, modification and commercial use with attribution.
- Its trust signals score 98/100, with no cautions. These come from repository metadata, not a code audit — read the skill file before letting an agent act on it.
sast-fileupload compared with similar skills
All 4 of these similar skills score higher than sast-fileupload; compare them before choosing.
| Skill | Score | Stars | Updated | Format |
|---|---|---|---|---|
| sast-fileupload (this skill)by utkusen | 89 | 1.3k | 6mo ago | SKILL.md |
| algorithmic-artby anthropics | 100 | 177.9k | 8d ago | SKILL.md |
| pptxby anthropics | 100 | 177.9k | 8d ago | SKILL.md |
| designby nextlevelbuilder | 100 | 130.2k | 9d ago | SKILL.md |
| ui-ux-pro-maxby nextlevelbuilder | 100 | 130.2k | 9d ago | SKILL.md |
Frequently asked questions
- How do I install sast-fileupload?
- Run
npx skills add utkusen/sast-skills --skill sast-fileupload. The install tabs above show the steps for each supported agent. - Which AI agents does sast-fileupload work with?
- It is written for Universal, as a SKILL.md file. Other agents that read the same format can often use it too.
- Is sast-fileupload safe to use?
- It is MIT-licensed and scores 98/100 on trust signals. Skills are instructions an agent will follow, so read the file before installing it and do not approve commands you do not understand.
- Is sast-fileupload still maintained?
- The repository was last updated about 6 months ago. That is recent enough to be usable, but agent tooling moves fast, so check the instructions against your agent's current version.
Skill content
View source on GitHubname: 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/pngwithout verifying the actual extension or file content — trivially bypassed by setting the header manually - Extension blocklist with gaps:
.phpis blocked but.php3,.php4,.php5,.phtml,.phar,.shtmlare not - Case-insensitive bypass: blocking
.phpbut allowing.PHP,.Php,.pHp - Double extension bypass:
shell.php.jpg— code extracts the last.jpgand considers it safe, but the server (Apache) serves it as PHP - Path traversal in filenames:
../../webroot/shell.phpstored 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)
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)
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
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
@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
import uuid
stored_name = str(uuid.uuid4()) + '.jpg' # extension is server-controlled, not user-controlled
Vulnerable vs. Secure Examples
Python — Flask
# 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].lower()
if ext not in ALLOWED:
abort(400)
f.save(os.path.join(UPLOAD_FOLDER, filename))
return 'uploaded'
Python — Django
# VULNERABLE: no validation on FileField
class DocumentForm(forms.ModelForm):
class Meta:
model = Document
fields = ['upload']
# VULNERABLE: manual save with no extension check
def upload(request):
f = request.FILES['file']
with open(f'media/uploads/{f.name}', 'wb+') as dest:
for chunk in f.chunks():
dest.write(chunk)
# SECURE: custom validator on FileField
def validate_file_extension(value):
ext = os.path.splitext(value.name)[1].lower()
if ext not in ['.png', '.jpg', '.jpeg', '.gif']:
raise ValidationError('Unsupported file extension.')
class DocumentForm(forms.ModelForm):
upload = forms.FileField(validators=[validate_file_extension])
Node.js — Multer (Express)
// VULNERABLE: no file filter, stored in public directory
const upload = multer({ dest: 'public/uploads/' });
app.post('/upload', upload.single('file'), (req, res) => {
res.send('uploaded');
});
// VULNERABLE: MIME type filter only (can be faked)
const upload = multer({
dest: 'uploads/',
fileFilter: (req, file, cb) => {
if (!file.mimetype.startsWith('image/')) return cb(null, false);
cb(null, true);
}
});
// SECURE: allowlist of extensions + storage outside web root
const ALLOWED_EXT = ['.jpg', '.jpeg', '.png', '.gif'];
const storage = multer.diskStorage({
destination: '/var/uploads', // not served by Express
filename: (req, file, cb) => {
const ext = path.extname(file.originalname).toLowerCase();
cb(null, `${uuidv4()}${ext}`);
}
});
const upload = multer({
storage,
fileFilter: (req, file, cb) => {
const ext = path.extname(file.originalname).toLowerCase();
cb(null, ALLOWED_EXT.includes(ext));
}
});
PHP
// VULNERABLE: no extension check, stored in web root
move_uploaded_file($_FILES['file']['tmp_name'], 'uploads/' . $_FILES['file']['name']);
// VULNERABLE: checking only content type header
if ($_FILES['file']['type'] !== 'image/jpeg') {
die('Invalid file type');
}
move_uploaded_file($_FILES['file']['tmp_name'], 'uploads/' . $_FILES['file']['name']);
// VULNERABLE: blocklist missing phtml/phar
$ext = strtolower(pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION));
$blocked = ['php', 'sh', 'py'];
if (in_array($ext, $blocked)) die('Blocked');
move_uploaded_file($_FILES['file']['tmp_name'], 'uploads/' . $_FILES['file']['name']);
// SECURE: allowlist + rename to UUID + outside web root
$allowed = ['jpg', 'jpeg', 'png', 'gif'];
$ext = strtolower(pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allowed)) die('Invalid extension');
$stored = '/var/uploads/' . bin2hex(random_bytes(16)) . '.' . $ext;
move_uploaded_file($_FILES['file']['tmp_name'], $stored);
Java — Spring Boot (MultipartFile)
// VULNERABLE: no validation, stored in web-accessible path
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) throws IOException {
Path path = Paths.get("src/main/resources/static/uploads/" + file.getOriginalFilename());
Files.write(path, file.getBytes());
return "uploaded";
}
// VULNERABLE: content type header only
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) throws IOException {
if (!file.getContentType().startsWith("image/")) throw new BadRequestException();
Files.write(Paths.get("uploads/" + file.getOriginalFilename()), file.getBytes());
return "uploaded";
}
// SECURE: allowlist + UUID rename + path outside web root
private static final Set<String> ALLOWED = Set.of("jpg", "jpeg", "png", "gif");
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) throws IOException {
String original = StringUtils.cleanPath(file.getOriginalFilename());
String ext = FilenameUtils.getExtension(original).toLowerCase();
if (!ALLOWED.contains(ext)) throw new BadRequestException("Invalid extension");
String stored = UUID.randomUUID() + "." + ext;
Files.write(Paths.get("/var/uploads/" + stored), file.getBytes());
return "uploaded";
}
Go
// VULNERABLE: no extension check, stored in static directory
func uploadHandler(w http.ResponseWriter, r *http.Request) {
file, header, _ := r.FormFile("file")
defer file.Close()
dst, _ := os.Create("static/uploads/" + header.Filename)
defer dst.Close()
io.Copy(dst, file)
}
// SECURE: allowlist extension + UUID rename + outside web root
var allowed = map[string]bool{"jpg": true, "jpeg": true, "png": true, "gif": true}
func uploadHandler(w http.ResponseWriter, r *http.Request) {
file, header, _ := r.FormFile("file")
defer file.Close()
ext := strings.ToLower(filepath.Ext(header.Filename))
if ext == "" || !allowed[ext[1:]] {
http.Error(w, "invalid extension", http.StatusBadRequest)
return
}
stored := "/var/uploads/" + uuid.New().String() + ext
dst, _ := os.Create(stored)
defer dst.Close()
io.Copy(dst, file)
}
Ruby on Rails
# VULNERABLE: no content type or extension validation
def upload
file = params[:file]
File.open(Rails.root.join('public', 'uploads', file.original_filename), 'wb') do |f|
f.write(file.read)
end
end
# SECURE: ActiveStorage with content type allowlist (Rails 6+)
has_one_attached :avatar
validates :avatar, content_type: ['image/png', 'image/jpg', 'image/jpeg']
# Note: still validate extension too — content_type is user-supplied in some configurations
# SECURE: CarrierWave with extension and content type allowlist
class AvatarUploader < CarrierWave::Uploader::Base
def extension_allowlist
%w[jpg jpeg png gif]
end
def content_type_allowlist
/image\//
end
end
C# — ASP.NET Core
// VULNERABLE: no extension c
Truncated for display — read the full file on GitHub.
Related Skills
algorithmic-art
177.9kCreating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems.
pptx
177.9kUse this skill any time a .pptx or .potx file is involved in any way — as input, output, or both. This includes: creating slide decks, pitch decks, or presentations; reading, parsing, or extracting text from any .pptx or .potx file (even if the extracted content will be used elsewhere, like in an em…
design
130.2kComprehensive design skill: brand identity, design tokens, UI styling, logo generation (55 styles, Gemini, Atlas Cloud, or MuAPI AI), corporate identity program (50 deliverables, CIP mockups), HTML presentations (Chart.js), banner design (22 styles, social/ads/web/print), icon design (15 styles, SVG…
ui-ux-pro-max
130.2kUI/UX design intelligence for web, mobile, and desktop. This skill should be used when designing, building, reviewing, or fixing interfaces, including pages, components, design systems, accessibility, interaction, responsive layout, typography, color, charts, and stack-specific UI implementation.
Trust signals
From repository metadata: license, adoption, age and documentation. Not a code audit — see the Safety scan above for what the skill file itself contains.
