From e0fcb05c144f0902d241c0aa8e33f57794246dd4 Mon Sep 17 00:00:00 2001 From: nmemmert <67961292+nmemmert@users.noreply.github.com> Date: Fri, 7 Nov 2025 09:48:01 -0500 Subject: [PATCH] archived zip fix and security updates --- __pycache__/pdf_renamer.cpython-313.pyc | Bin 73600 -> 82542 bytes archives/README.md | 18 ++ pdf_renamer.py | 250 ++++++++++++++++++++---- 3 files changed, 233 insertions(+), 35 deletions(-) create mode 100644 archives/README.md diff --git a/__pycache__/pdf_renamer.cpython-313.pyc b/__pycache__/pdf_renamer.cpython-313.pyc index 274461beb265793935cd31704ede5e8876c4877e..3108960c55e8358dbfee4740115084bdc14c9b33 100644 GIT binary patch delta 18517 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 T3VB-`_YSLTGjQ)1WRU(ZI!(vf delta 10413 zcma(%3s{ubvH#hB-%nt7VF6c`m+XQFh{{VvK;$K;{BG1hvaYZo0=sY)sX&#y; zQG1eawP|YFo7?J5Qcbx{qHR(WjkoVW&BJc_+N4cyAEvpe$!(kT-rSk`)xnJmdZrVfQGO>P(WkLg5l5 z3p+(pNCNx5=(HtDNy`=3Ll8Qh19`l`UJw_ER>{Vu#B7sYN{%s54?8QS22vzP9FyN! zkR?c|oo=&Gs#s2VcfloaY+9%12H5n@e7DRd_zi*}E$CdFC3NP;`WC7LVFQtz?0#jQ zX`z%6msE(dMVFMxqRP_jtWHm7zLbq}rJPbx%9R#Lc}ojeF0BMigR}}@qqG`eQ-V|tU(M1QfGwTQ_M>dL z!J1wZTe4J}7M~JrjnnJm^tE&7ZEj9rWhyNpwYR3Y1<8$wsbC#Zm!%a2>wJo6z$L8c5O0A9a1MApI*{S zGKJ2i8o}=n1X{|iEx)sLzO74ymShM3OY2G-NZa4#RuAbBR*4xx5AobG-mS9{_GSIn z@ok37%FVTPm7U&jfQCEU{3C(-!NC9>=^X0rDWrj*e=txO4&C_KC)4SYq1&Rmu>W%b z->%S(Fl`1KuO9DC=n{R^(4?yotN{>J4ut&Ov=X1|@L3!P1Zgd_JR)sCgaH`>-SRk% zfC-BLTrL-FLb49YGEsu(jck$alA~+JhX6tlH1G#UTe!BAL=Ia?mcl>C*4n?R=rFOD z!q!p?^592OdIdPYDlY-uDPx-qr&5joVkaLou4WVFG<~mnMs*stCb@0#hlaKFwcdtG zS955GYp>N69`TQihFzmW-Jrj2*GS0K=F!p$kV7lk^U1D8R0&@)EVLfmUI0;b|876+ z4bNX=qC5(&(kG8gZvo~V@E0Cs=_%5RDWm;?mMLT6p_a$hNA^tQRX)GsdG|z8-D}3R ze>DmEq=Sx!93O^_(BZzC@>;Fl^Zq?4-Mp1;>h-;dvxL zgJEA!Xf)VO8`xk$3u$Dh3vx*l`%QsMbtkY5j4KyECz3Mu{i0eDVjmYdQ~BKUP;^bGL&~W0FR0u!1g@|?nQ7Pg8c~Y2XI29 z4`TZv08#azf233n6+6SkxE|^c@2GS`5rvFD=DUATb1%YiZ-ONQ)v)?Rtveu$xwr{D2ZCth?CWa5r zS2Zh;uM`*gSz?yaqrmm5z+6=eSy8!xU0$hY3zuhdk$;YfnAIbfGrOFBM#OUZD2TWe zQ(QG4+Q(PZP!`g4_^ge)ITF<1$_+Uye-Q~FGo2Y<$vBdcl=oeT_=Q({{VCk zqq~SXlzIDKMvnpM&$uDWKvQ=1>k4`A=Wooq5*J%nk^-BgV;e!2mQqzri(DpGr^8DFvucuve5fmy@nbBL?d zqnteg^kH^*r9G_)x<%(Lyn_BK;QoPnMN8}`_kp0h8tE4S%MTL$gyPKe&T29J7O<)y z2cw6u56(U+LIjD5d;5mQ2Ua^2mE7SgyZgFEC^~4WL!d`60>C3imFNp;Ct?}^%$%Hk zw28gArjFQ{qUH-_I51Trzk)N-XOI(jbo)$*FoRp_3qoWX9_^!nZaNM$D|@ykpZD#8 zDaJoI4u+R&K>&|H!1M}L&{{>@0P*9`2TNi9wItOfDPkc(QmELK(hO$@_U0e#lLHHA zi;^W2_rxQzg!=4VCrDJ=(+3d_LTDJ}&dkv09~cPjh9HMT`aK{l1Zub#8vBGx2J<2J zf$BF6TaN5GHgQ~ zf~7nnuzB<{hGWhLKGJG0=2CMl(fo@=R-SbEYCkfC!mxNRk6&rqHo~z0{mq zkRu5}ZH;y|>p%)Dk$)h6NfU&iPD+w&9U1KKUCGSTV%!EZ1Z0hFwv)%+Msas#$Bri} zN_^bGf2dpn_9c6Lw04i0o|(rehXMICu;TR@x!aNK2e8+%&JU{RaXPSPg!;QiTs<^2 zSm~l&Y-D|-CaMZh8lvo_^_|)TnuPr}vE0`B6|=!Is@xUo3r02FeH8bX=QvT-ZrV2z zh-!EE!+h6gf+suKT9JZbHfrhNf_?GO*}y(-wc8tk7RI9hiO$RitlrbMz%~iUOtg+? z0qhf|71^p4?AEp?)q*B5=~^;dyy02ZgtqIiR+g|aee8X`V6z3jDHaug6oK7_B| z3gfO@%Ly4_+k82Yq}<`FQG7tyOTOjyUDy+S0}UY z*tHVy_poQS=Q_WDESTe9dIOnRH_vtG!-#%@UE5x`UzaO)!W>+hy1B}IpVeW53XDA@3zJzWKy{1dyZxr8DisDM9_ zJ!D_Vp0(Q|UK`0^!Ch+R?llrM+q7#zLYClFN?J+BivW5U(chb*S|k~G%f=4(S|hm@ zVr8!m=$Jj*%6`a$zk!1+iHuLbJuR>h}VB$V^8nOkafWRw96!l?YZpj zMk_P+yA9VbYIA>r0q4d)UN!f+8C!Jh@y!~>`cvWy-o4MP^s3uEGXoh|S#u&&518Y# zhno&74(r8Cm|x34o{c4j2!mFU=Gq1?S*_w9}YT55wiwYi3(UP1QOO0Tnv*#RedM8nz!oW z0|r9&c>BpY?JJ>GGkR+o%8qj!8>*Ts$~c@H$+3}z?2R8-nKYal=X-3JONdR$IGn_C zXwDo>6mvV;NzJiIlGSLxiuO{zhO>0p_39+GtSX$kr5P`nE>K)6oL_8pfNPdNQeDUF z{w2he4A;!H-h}zr%m!5}YS)WtT9IOU1q zx=YxnYtJ6no{6clma@l(l2lHqDwrlOQ{Jk7jDCv#9{gP1Ts{9|G|7smaW|ZtS!+bs$(dlS9cu^ca$GuISsdmZT; zy*aRro4mR6x6zx~UdGMk=Do$z^?s%uZ9!_~bp}(Z_z6EGar)*tCX+G8WW3k|J?`u| zOS~yYcL^I(q;0bIvD`e1l{x_DyWO4&o+)OOGhVL~rGbVYyuFm%!d|-Fo^m~h8#y}S zr#kyqK6Brk`NgZOpo{H@XFs>6u+3jcSM&<3>WSr=Z%RZBC z9Ud!bF~`_NrK~Ak!(1%;+L*0S-wFx)^-1S-Q~#u-#yOIj=8hSc)I1l9l0<#$UF`6_ zvP?x=;SJZbkt=8m9~nI?9QZUT?-Sc7&fg?)>&#R>zSG{4ef>hh3Kv|Cf(v_u>z35c zg?-BCl0QC)dFH3v!40B|Q|+)QtF8AFy7P3ir|-@+NA*KA)D;Ma`+~jH%YJ!xk>(@V z=gl;oS?;M%E`l~B@NhSjD^Kg;nZC$&-E(>klIV*F@G8{?_q^xeJF11EQ+RjZ$WA~Q z2q1KLd-C-KdqQ(B^wi95xi_0Q#_zn>Oq%#uF5qs1YcLK&v*CgA>}>=dp%{l_(HsOk z%@zUca>1PwnazmK1(3=v-8V^IVE?xNf5};P;y|%-ucFe$etjU>z!S?qBKLjl;{%0; zd$Gx{{P(c3``gJGcK`ij>k17H z4FsTk7`4pkZsv*@mh)IB3JZI4TrwLQd?l4_eyD1-X8|uO$~9F8O>^scm|8|fNDHIt zu28V2uQ#e4^!Eqg$`YQ@21_~UQrBXyRczHkhZ8ju)z93C;F=m$?)DG#^THsVWb|P6 zmLCF#Cy&=Kqbl5jFf9QMMH5E?gG0WVo<0;2H3WwG!XZdo;fbp_s=;N`K#yGO)xrIZ z-+{xlk$rq{G3aXXp+@p!HhQQ@6%}DDcJ7d0&G#jb)%a6c!j%O78o3f|n9&iIN$IUH zgMYwZ_$Nj|_#0l6b}LlHQ(Du6dFgA~(o2h$KA=0aXi}Sc#679azT{pTd(5BI=1yr7 zA23`>Td?19X~Dt?ZR+?3hervi8}DZiD^*iDg-^7w#3SjZO+?6AazzxDH<9;mViiX+ zNyd1`kv|gClq2();Yy1rq-9KRWjnsIKHqApo^oa!+j4ZvvCgBNPi3FXKaqdRH<@4m zL&rq>)`?BqCbr!&(QxZUr;q*eD<7y{Pgpx`W^G?h?R;xdNo;0(qD8A`9w*WYCbf%y zxw`dYUe}q>^NACi+9#yVlbgH~+jdND+VQ0Ec;fMr6Sm{SrxH($JZU@~n#}8pjpUis z=D-@+&%WBE|3DCQZHm8N{k)R(KenC(S;^NvCw}z)TKK-hU=)(qlQ--wAHDBj+GF08 z$%4C^{Ox@$zf&qz#g`Tp#>V8c%da_~eb#?&(OJiYyE#Ug(I`84EPZTJTlCSi4tjnV z9)sQaVr>;Q3g^VM{xZcmr?o#vb*?zCKSO&yU5zkP518`>TA-dUtA2eMS}68nHt^=>lqzw1;Di~%YOncmIS z4=mHYTcm{vzgwmcW~u%{YV1L$>OIc;o>K*=_c-f&x%#05)q6!X_9266T8TqV8&rUr zPS9f8p&!cAOlN6fuG9H?suZV})u@K6#VZwRAYZ9g0qUv}S+5!s^kKJnHMvF`&K9p_ ztHSQ_KYYWWb)1mPbFz#3HlBS1&nlSd$pz%_c>a@H6sEH=G6^0a zkhcIpG}%8ovNJ^c_6B_ZE{tftkx)OpwHkr-XB)Wh#6{9RcI=EJYj)9aM~8{cOpOMR z1wr!oFVDCXHBaIAPa{C(Lv+G*Ld7&RG^Z}Q9XlOHCJaYWH7qPNIzsmWx`I`{P{E7D zv|#+U7bcXN0c5TwY~#zmY(2J5rJZ91J^@@IxWxg^> z^2Z~uOsFk5!KemaN^$PH${j{)wD+Dil@?h(*bpSi6;T>22-fQkhOg;1uqnYC7yIgDJctwYi zhG5P3=AZo!0h?@pb!1h+1AqbLA-_D?`Fl>0&cP8ea>Yp8Y`f$ZXgL6XZq=zFeH$-Da!M_)S3= z1#1~f!W-FE8#`O9PmmP&TAG(+po}XMA|@Nr8@)oDQX0v$k!=Pcevy)ELjBD)HkhW5 z9JLdZ>VCLnSR<`AQs{>Fuade##56UBUj=(L9&MX!e#N+QZh2`!q&bnS8q>=fs=_sM zFY#$RDd#JY3gE#ygY38|bRB|D7&bibY(}-+q1{2WSo$asA46T;3#_0kt>DOvj2#|( zBgK8dFt%R}#rcC}2g(kWUoc*lV|j`Y20abL8!r&98tDm__wqViN| zvS8g0wG-RBCsP8yNC;ff2@cORlyjGI2e3Dxs5{bcCFx~&V?rap_G_5yBZ5~zJC{GG z82`^eyDRqXPrCEus05 zVF&SyorUrCnvpLUg4Y808hZ(S1eEPD%J%o!riX>=!4UWoo{yVgKakRvf&>Iq3wV*XWoO)4{Hf2sZqMkHo>=)mfXN(sO1#elB(7q1s zebbUPX~~*OcOEM{T6S#Z(Unh$C-o=vr>ZC2b!Vj?roH65nC?BKowC{^Wrxcms}HY! z-2JAtVA5JJWot~1G<~`0$j;YndG92o9$7SzkQw{A!9-Hw@!MWER=i_OoJcBrKIL^| z%{!Q4FMM3{n$|sKupVlFb1|V@z)kkKhII1Z3yZl2=I-rrTrvE`UW1wA7arMBeFJDO zDkFoHq!7H^qm?8x+p~>(>!=nl7yNA*ZZSOsAgUZ4?dzs%BJWg^AD8iPqTLl^^`WkQ`IjGj_wi>c{P!F5PHY`S@Cbqkf};S&KI5gq^T>#A zG37uuiLG-8Fk(hk6jFi!&mwp>MN!3ax`7^DjypVxU?AWgjvE(6bw@=y8qB5JkusZS zXjcw~x<>~BtLbSVz<(ipvocb-iX2TA6!#hKGVHHA*nFV*(AcCg?^Mn>AEGYLffh zW2?yuqK4#?)RV{u)nuI;tkz`Nr@N}uk%d>23E4`ns^JQJbv+3yNW#@!syzx)JUya% zj;= SAVE_BATCH_SIZE: + save_jobs() + save_counter = 0 def analyze_failed_extractions(log_path): """ @@ -268,6 +332,10 @@ def map_pattern_10(match1, match2): } ALLOWED_EXTENSIONS = {'pdf', 'zip'} +ALLOWED_MIME_TYPES = { + 'pdf': 'application/pdf', + 'zip': 'application/zip' +} # Global jobs storage (in production, use database) job_queue = {} @@ -283,10 +351,48 @@ def login_required(f): return f(*args, **kwargs) return decorated_function -def allowed_file(filename): - return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS +def allowed_file(filename, file_stream=None): + """ + Validate file based on extension and optionally MIME type. + + Args: + filename: The name of the file + file_stream: Optional file stream to check MIME type + + Returns: + bool: True if file is allowed, False otherwise + """ + # Check extension + if '.' not in filename: + return False + + ext = filename.rsplit('.', 1)[1].lower() + if ext not in ALLOWED_EXTENSIONS: + return False + + # Additional MIME type validation if file stream provided + if file_stream: + try: + import magic + file_stream.seek(0) + mime_type = magic.from_buffer(file_stream.read(2048), mime=True) + file_stream.seek(0) + + expected_mime = ALLOWED_MIME_TYPES.get(ext) + if expected_mime and mime_type != expected_mime: + print(f"MIME type mismatch: expected {expected_mime}, got {mime_type}") + return False + except ImportError: + # python-magic not installed, skip MIME check + pass + except Exception as e: + print(f"Error checking MIME type: {e}") + return False + + return True def extract_names_from_pdf(pdf_path): + reader = None try: reader = PdfReader(pdf_path) @@ -323,6 +429,13 @@ def extract_names_from_pdf(pdf_path): except Exception as e: return {'error': f'PDF read error: {str(e)}'} + finally: + # Properly close PDF to prevent resource leaks + if reader is not None: + try: + reader.stream.close() + except: + pass def extract_student_info_from_text(text): """ @@ -506,13 +619,14 @@ def extract_student_info_from_text(text): return None def process_job(job_id, files): + temp_extract_dirs = [] try: print("=== DEBUG: process_job STARTED ===") - job_queue[job_id]['status'] = 'processing' - job_queue[job_id]['progress'] = 'Starting job processing...' + with job_lock: + job_queue[job_id]['status'] = 'processing' + job_queue[job_id]['progress'] = 'Starting job processing...' save_jobs() # Persist initial progress processed_files = [] - temp_extract_dirs = [] # Create base Episode directory for this job episode_dir = os.path.join(app.config['UPLOAD_FOLDER'], 'Episode') @@ -605,7 +719,9 @@ def process_job(job_id, files): processed_files.append(('success', os.path.relpath(new_path, episode_dir), new_path)) processed_count += 1 job_queue[job_id]['progress'] = f'Organized {processed_count}/{total_files} files' - save_jobs() # Persist progress update + # Batch saves - only every 5 files + if processed_count % 5 == 0: + save_jobs() print(f"DEBUG: Organized file {processed_count}/{total_files}") print(f"DEBUG: Found {len(pdf_files_found)} PDF files in ZIP: {pdf_files_found}") @@ -643,7 +759,9 @@ def process_job(job_id, files): processed_files.append(('success', os.path.relpath(new_filepath, episode_dir), new_filepath)) processed_count += 1 job_queue[job_id]['progress'] = f'Organized {processed_count}/{total_files} files' - save_jobs() # Persist progress update + # Batch saves - only every 5 files + if processed_count % 5 == 0: + save_jobs() except Exception as e: processed_files.append(('skipped', filename, f'PDF processing error: {str(e)}')) job_queue[job_id]['progress'] = f'Failed to process PDF {filename}: {str(e)}' @@ -656,7 +774,8 @@ def process_job(job_id, files): print(f"DEBUG: Starting ZIP creation, successful_files: {len(successful_files)}") if successful_files: try: - zip_output = os.path.join(app.config['UPLOAD_FOLDER'], f'renamed_{job_id}.zip') + # Create ZIP in archives folder + zip_output = os.path.join(app.config['ARCHIVE_FOLDER'], f'renamed_{job_id}.zip') print(f"DEBUG: Creating ZIP at {zip_output}") with zipfile.ZipFile(zip_output, 'w', zipfile.ZIP_DEFLATED) as zip_out: for root, dirs, files_in_dir in os.walk(episode_dir): @@ -670,6 +789,24 @@ def process_job(job_id, files): job_queue[job_id]['progress'] = f'Created ZIP archive with {len(successful_files)} organized files' save_jobs() # Persist completion progress print(f"DEBUG: ZIP creation completed successfully") + + # Clean up Episode directory after successful ZIP creation + try: + import shutil + shutil.rmtree(episode_dir) + print(f"DEBUG: Cleaned up Episode directory: {episode_dir}") + except Exception as cleanup_error: + print(f"WARNING: Failed to clean up Episode directory: {cleanup_error}") + + # Clean up original uploaded files + for file_info in files: + try: + if os.path.exists(file_info['path']): + os.remove(file_info['path']) + print(f"DEBUG: Removed uploaded file: {file_info['path']}") + except Exception as file_cleanup_error: + print(f"WARNING: Failed to remove uploaded file: {file_cleanup_error}") + except Exception as e: job_queue[job_id]['progress'] = f'Error creating ZIP: {str(e)}' job_queue[job_id]['status'] = 'error' @@ -679,17 +816,29 @@ def process_job(job_id, files): job_queue[job_id]['progress'] = 'No files to archive - all files were skipped' print(f"DEBUG: No successful files to archive") - job_queue[job_id]['status'] = 'completed' - job_queue[job_id]['processed_files'] = processed_files - job_queue[job_id]['skipped'] = [(p[1], p[2]) for p in processed_files if p[0] == 'skipped'] + with job_lock: + job_queue[job_id]['status'] = 'completed' + job_queue[job_id]['processed_files'] = processed_files + job_queue[job_id]['skipped'] = [(p[1], p[2]) for p in processed_files if p[0] == 'skipped'] save_jobs() # Persist completed job print(f"DEBUG: Job {job_id} completed successfully. Processed: {len(processed_files)}, Skipped: {len(job_queue[job_id]['skipped'])}") except Exception as e: - job_queue[job_id]['status'] = 'error' - job_queue[job_id]['progress'] = f'Job failed with error: {str(e)}' + with job_lock: + job_queue[job_id]['status'] = 'error' + job_queue[job_id]['progress'] = f'Job failed with error: {str(e)}' save_jobs() # Persist error status print(f"Job {job_id} failed: {str(e)}") # Debug logging + finally: + # Clean up temporary directories + for temp_dir in temp_extract_dirs: + if os.path.exists(temp_dir): + try: + import shutil + shutil.rmtree(temp_dir) + print(f"DEBUG: Cleaned up temporary directory: {temp_dir}") + except Exception as cleanup_error: + print(f"ERROR: Failed to clean up {temp_dir}: {cleanup_error}") @app.route('/login') def login(): @@ -1181,34 +1330,65 @@ def api_jobs(): @app.route('/download/') @login_required def download(filename): + """Download a completed job's ZIP file with path traversal protection""" + # Prevent path traversal attacks + if '..' in filename or '/' in filename or '\\' in filename: + return "Invalid filename", 400 + # Security check: only allow downloading files that belong to completed jobs - for job_id, job in job_queue.items(): - if job.get('status') == 'completed' and job.get('zip_name') == filename: - file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) - if os.path.exists(file_path): - return send_from_directory(app.config['UPLOAD_FOLDER'], filename, as_attachment=True) + with job_lock: + for job_id, job in job_queue.items(): + if job.get('status') == 'completed' and job.get('zip_name') == filename: + # Use ARCHIVE_FOLDER instead of UPLOAD_FOLDER + file_path = os.path.join(app.config['ARCHIVE_FOLDER'], filename) + # Verify the resolved path is within ARCHIVE_FOLDER + resolved_path = os.path.abspath(file_path) + archive_folder = os.path.abspath(app.config['ARCHIVE_FOLDER']) + + if not resolved_path.startswith(archive_folder): + return "Access denied", 403 + + if os.path.exists(resolved_path): + return send_from_directory(app.config['ARCHIVE_FOLDER'], filename, as_attachment=True) + return "File not found or access denied", 404 @app.route('/delete/', methods=['POST']) @login_required def delete_job(job_id): - if job_id in job_queue: - job = job_queue[job_id] - # Only allow deletion of completed jobs - if job.get('status') == 'completed': - # Clean up the ZIP file if it exists - if job.get('zip_name'): - zip_path = os.path.join(app.config['UPLOAD_FOLDER'], job['zip_name']) - if os.path.exists(zip_path): - os.remove(zip_path) - # Remove the job from the queue - del job_queue[job_id] - save_jobs() # Persist job deletion - flash('Job deleted successfully.', 'success') + with job_lock: + if job_id in job_queue: + job = job_queue[job_id] + # Only allow deletion of completed jobs + if job.get('status') == 'completed': + # Clean up the ZIP file from archives folder + if job.get('zip_name'): + zip_path = os.path.join(app.config['ARCHIVE_FOLDER'], job['zip_name']) + if os.path.exists(zip_path): + try: + os.remove(zip_path) + print(f"DEBUG: Deleted archive file: {zip_path}") + except Exception as e: + print(f"ERROR: Failed to delete archive: {e}") + flash('Job deleted but archive file could not be removed.', 'warning') + + # Clean up any remaining uploaded files + for file_info in job.get('files', []): + if 'path' in file_info and os.path.exists(file_info['path']): + try: + os.remove(file_info['path']) + print(f"DEBUG: Deleted uploaded file: {file_info['path']}") + except Exception as e: + print(f"ERROR: Failed to delete uploaded file: {e}") + + # Remove the job from the queue + del job_queue[job_id] + save_jobs() # Persist job deletion + flash('Job deleted successfully.', 'success') + else: + flash('Only completed jobs can be deleted.', 'error') else: - flash('Only completed jobs can be deleted.', 'error') - else: - flash('Job not found.', 'error') + flash('Job not found.', 'error') return redirect('/jobs') @app.route('/completed-jobs')