Skip to content

Commit cf937de

Browse files
xusheng6claude
andauthored
Reject archives containing only one file (#143)
Add validation to reject zip archives that contain only a single file, since users should upload single files directly without wrapping them in an archive. Changes: - Add get_archive_file_count() and is_single_file_archive() to archive.py - Use is_single_file_archive() in crackme and solution controllers - Update upload guidance in templates to clarify this rule - Update FAQ with this rejection reason Closes #139 Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 53a1910 commit cf937de

8 files changed

Lines changed: 141 additions & 8 deletions

File tree

app/controllers/crackme.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from app.services.recaptcha import verify as verify_recaptcha
2222
from app.services.limiter import limit
2323
from app.services.view import FLASH_ERROR, FLASH_SUCCESS, validate_required
24-
from app.services.archive import is_archive_password_protected
24+
from app.services.archive import is_archive_password_protected, is_single_file_archive, is_unsupported_archive
2525
from app.services.discord import notify_new_crackme
2626
from app.controllers.decorators import login_required
2727

@@ -190,11 +190,21 @@ def upload_crackme_post():
190190
flash('This file is too large!', FLASH_ERROR)
191191
return render_template('crackme/create.html')
192192

193+
# Check for unsupported archive formats (RAR, tar, etc.)
194+
if is_unsupported_archive(file_data):
195+
flash('RAR and tar archives are not supported. Please upload a ZIP file for multiple files, or upload single files directly.', FLASH_ERROR)
196+
return render_template('crackme/create.html')
197+
193198
# Check for password protection
194199
if is_archive_password_protected(file_data):
195200
flash('Password-protected archives are not allowed. Do NOT add a password yourself - the server handles this automatically.', FLASH_ERROR)
196201
return render_template('crackme/create.html')
197-
202+
203+
# Check for single-file archives
204+
if is_single_file_archive(file_data):
205+
flash('Archives containing only one file are not allowed. Please upload the file directly without wrapping it in an archive.', FLASH_ERROR)
206+
return render_template('crackme/create.html')
207+
198208
# Store the uploaded file size
199209
size = len(file_data)
200210

app/controllers/solution.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from app.services.recaptcha import verify as verify_recaptcha
1515
from app.services.limiter import limit
1616
from app.services.view import FLASH_ERROR, FLASH_SUCCESS
17-
from app.services.archive import is_archive_password_protected, is_pe_file
17+
from app.services.archive import is_archive_password_protected, is_pe_file, is_single_file_archive, is_unsupported_archive
1818
from app.services.discord import notify_new_solution
1919
from app.controllers.decorators import login_required
2020

@@ -90,11 +90,21 @@ def upload_solution_post(hexidcrackme):
9090
print(f"Error reading file: {e}")
9191
abort(500)
9292

93+
# Check for unsupported archive formats (RAR, tar, etc.)
94+
if is_unsupported_archive(data):
95+
flash('RAR and tar archives are not supported. Please upload a ZIP file for multiple files, or upload single files directly.', FLASH_ERROR)
96+
return redirect(f'/upload/solution/{hexidcrackme}')
97+
9398
# Check for password-protected archives
9499
if is_archive_password_protected(data):
95100
flash('Password-protected archives are not allowed. Do NOT add a password yourself - the server handles this automatically.', FLASH_ERROR)
96101
return redirect(f'/upload/solution/{hexidcrackme}')
97102

103+
# Check for single-file archives
104+
if is_single_file_archive(data):
105+
flash('Archives containing only one file are not allowed. Please upload the file directly without wrapping it in an archive.', FLASH_ERROR)
106+
return redirect(f'/upload/solution/{hexidcrackme}')
107+
98108
# Check for PE files (patched binaries are not allowed)
99109
if is_pe_file(file.filename, data):
100110
flash('Executable files (PE binaries) are not allowed as solutions. '

app/services/archive.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
"""
44

55
import zipfile
6+
import tarfile
67
import io
78
import struct
89

10+
911
# PE file extensions (case-insensitive)
1012
PE_EXTENSIONS = {'.exe', '.dll', '.sys', '.scr', '.ocx', '.com', '.drv', '.cpl', '.efi'}
1113

@@ -57,6 +59,116 @@ def is_pe_file(filename: str, file_data: bytes) -> bool:
5759
return False
5860

5961

62+
def _is_metadata_file(filename: str) -> bool:
63+
"""Check if a file is a metadata file that should be ignored when counting.
64+
65+
Args:
66+
filename: The filename/path within the archive
67+
68+
Returns:
69+
True if the file is a metadata file, False otherwise
70+
"""
71+
# macOS resource fork files and metadata
72+
if filename.startswith('__MACOSX/'):
73+
return True
74+
# macOS AppleDouble files (resource forks)
75+
basename = filename.rsplit('/', 1)[-1]
76+
if basename.startswith('._'):
77+
return True
78+
# macOS .DS_Store files
79+
if basename == '.DS_Store':
80+
return True
81+
return False
82+
83+
84+
def is_rar_file(file_data: bytes) -> bool:
85+
"""Check if file is a RAR archive by magic bytes."""
86+
# RAR magic: "Rar!" (0x52 0x61 0x72 0x21)
87+
return file_data[:4] == b'Rar!'
88+
89+
90+
def is_tar_file(file_data: bytes) -> bool:
91+
"""Check if file is a tar archive (including .tar.gz, .tar.bz2, .tar.xz)."""
92+
# Check for gzip magic (1f 8b) - could be .tar.gz
93+
if file_data[:2] == b'\x1f\x8b':
94+
try:
95+
with tarfile.open(fileobj=io.BytesIO(file_data)) as tf:
96+
return True
97+
except Exception:
98+
return False
99+
100+
# Check for bzip2 magic (42 5a 68) - could be .tar.bz2
101+
if file_data[:3] == b'BZh':
102+
try:
103+
with tarfile.open(fileobj=io.BytesIO(file_data)) as tf:
104+
return True
105+
except Exception:
106+
return False
107+
108+
# Check for xz magic (fd 37 7a 58 5a 00) - could be .tar.xz
109+
if file_data[:6] == b'\xfd7zXZ\x00':
110+
try:
111+
with tarfile.open(fileobj=io.BytesIO(file_data)) as tf:
112+
return True
113+
except Exception:
114+
return False
115+
116+
# Check for plain tar (ustar at offset 257)
117+
if len(file_data) > 262 and file_data[257:262] == b'ustar':
118+
return True
119+
120+
return False
121+
122+
123+
def is_unsupported_archive(file_data: bytes) -> bool:
124+
"""Check if file is an unsupported archive format (RAR, tar, etc.).
125+
126+
Only ZIP files are supported for multi-file uploads.
127+
"""
128+
return is_rar_file(file_data) or is_tar_file(file_data)
129+
130+
131+
def get_archive_file_count(file_data: bytes) -> int | None:
132+
"""Get the number of files in a ZIP archive.
133+
134+
Returns None if the file is not a valid ZIP archive.
135+
Excludes metadata files (macOS __MACOSX, .DS_Store, ._ files) from the count.
136+
137+
Args:
138+
file_data: The binary content of the archive file
139+
140+
Returns:
141+
Number of files in the archive, or None if not a valid ZIP archive
142+
"""
143+
try:
144+
with zipfile.ZipFile(io.BytesIO(file_data), 'r') as zf:
145+
file_count = sum(
146+
1 for info in zf.infolist()
147+
if not info.is_dir() and not _is_metadata_file(info.filename)
148+
)
149+
return file_count
150+
except zipfile.BadZipFile:
151+
return None
152+
except Exception:
153+
return None
154+
155+
156+
def is_single_file_archive(file_data: bytes) -> bool:
157+
"""Check if an archive contains only a single file.
158+
159+
This is used to reject archives that unnecessarily wrap a single file,
160+
since users should upload single files directly without archiving them.
161+
162+
Args:
163+
file_data: The binary content of the archive file
164+
165+
Returns:
166+
True if the archive contains exactly one file, False otherwise
167+
"""
168+
file_count = get_archive_file_count(file_data)
169+
return file_count == 1
170+
171+
60172
def is_archive_password_protected(file_data: bytes) -> bool:
61173
"""Check if an archive file is password-protected.
62174

templates/crackme/create.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ <h3>Quick Rules</h3>
1717
<li>You must be able to <b>solve your own crackme</b></li>
1818
<li>Must execute without errors, <b>no network connections</b> except localhost</li>
1919
<li>Provide clear information about input/output and how to solve it</li>
20-
<li>Do NOT password-protect your archive (server handles this)</li>
20+
<li>Upload single files directly. If you have multiple files, put them in a zip archive without a password (server handles encryption)</li>
2121
</ol>
2222

2323
<p>Read the full <a href="/upload/crackmerules">crackme submission rules</a> for detailed guidelines.</p>

templates/faq/faq.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ <h3 id="writeup-rejected">Why was my writeup rejected? <a href="#writeup-rejecte
7676
<li>Only providing a password, serial, key, or executable without explanation</li>
7777
<li>Patching the binary when the expected solution is to keygen or find the password</li>
7878
<li>Insufficient detail about the solving process</li>
79-
<li>Password-protected archive (server handles this automatically)</li>
79+
<li>Single file wrapped in an archive, or password-protected archive</li>
8080
</ul>
8181
<p>Review the <a href="/upload/writeuprules">writeup submission rules</a>, improve your writeup with detailed explanations, and resubmit. Remember: we want others to learn from your solution!</p>
8282
<div class="divider"></div>
@@ -112,7 +112,7 @@ <h3 id="crackme-rejected">Why was my crackme rejected? <a href="#crackme-rejecte
112112
<p>Review the complete <a href="/upload/crackmerules">crackme submission rules</a>, fix the issues, and resubmit. If you're unsure why your crackme was rejected, contact us at crackmesone@gmail.com.</p>
113113
<div class="divider"></div>
114114
<h3 id="upload-size-limit">What is the maximum file size for uploads? <a href="#upload-size-limit" class="anchor-link">#</a></h3>
115-
<p>The maximum file size for both crackme and writeup uploads is <b>10 MB</b> (10,485,760 bytes). If you need to include additional files or resources, compress them into a single archive. Do not password-protect your archive - the server handles compression and password protection automatically.</p>
115+
<p>The maximum file size for both crackme and writeup uploads is <b>10 MB</b> (10,485,760 bytes). Upload single files directly. If you have multiple files, put them in a zip archive without a password (server handles encryption).</p>
116116
<div class="divider"></div>
117117
<h3 id="getting-started">How do I get started with reverse engineering? <a href="#getting-started" class="anchor-link">#</a></h3>
118118
<p>If you're new to reverse engineering, here are some steps to get started:</p>

templates/rules/crackmerules.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ <h3>Requirements</h3>
4848

4949
<li><b>Educational purpose only.</b> No cracks/keygens for commercial software, game trainers, or DRM circumvention tools.</li>
5050

51-
<li><b>Maximum file size: 5 MB.</b> Compress multiple files into a single archive. Do NOT password-protect it (server handles this).</li>
51+
<li><b>Maximum file size: 5 MB.</b> Upload single files directly. If you have multiple files, put them in a zip archive without a password (server handles encryption).</li>
5252

5353
<li><b>English language.</b> Display text and upload description must be in English.</li>
5454
</ol>

templates/rules/solutionrules.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ <h2>Writeup Submission Rules</h2>
1010
<p>It is important to think of these more as walkthroughs than simple solution submissions. Essentially, another user should be able to open your writeup and understand how you got there.</p>
1111
<ol>
1212
<li>Writeups should include text, doc or docx, md, pdf, etc. files detailing how you solved the crackme. Writeups can contain an executable file but should not exclusively contain an executable file.</li>
13-
<li>Writeups should not be compressed with a password. Our server handles the compression and passwords. Compressing is okay if you want to submit multiple files, but do not make it password-protected.</li>
13+
<li>Upload single files directly. If you have multiple files, put them in a zip archive without a password (server handles encryption).</li>
1414
<li>Writeups should contain more than just a flag, password, serial, binary, etc. They should contain detailed information about how you got to a writeup.</li>
1515
<li>Check the writeup to ensure it adequately solves the crackme. For example, if a crackme requests a keygen and you submit a detailed writeup and executable that serves as the keygen, please ensure the keygen produces valid keys.</li>
1616
<li>While external links to blog posts or websites are allowed, we would prefer if most of the writeup information was self-contained. If you'd like to copy and paste some of the stuff from your blog/website into your writeup and leave a link back to your blog/website, you are more than welcome to do so.</li>

templates/solution/create.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ <h2>Upload a Writeup</h2>
1313
<li>Please do NOT submit a patched crackme unless that crackme specifies that patching is intended (patchme).</li>
1414
<li>Please do NOT exclusively submit a password, serial, key, executable, solving script, etc. We want people to be able to learn from your writeup.</li>
1515
<li>Please submit your writeup in English.</li>
16+
<li>Upload single files directly. If you have multiple files, put them in a zip archive without a password (server handles encryption).</li>
1617
</ol>
1718

1819
<p>Read a full list of rules <a href="/upload/writeuprules">here</a></p>

0 commit comments

Comments
 (0)