@@ -703,6 +703,279 @@ async function testPerformance() {
703703 }
704704}
705705
706+ // ============================================================
707+ // SECURITY EXPLOIT TESTS
708+ // ============================================================
709+
710+ /**
711+ * SECURITY TEST 1: Unauthenticated send must be blocked
712+ */
713+ async function testUnauthenticatedSendBlocked ( ) {
714+ logSection ( 'SECURITY 1: Unauthenticated Send Blocked' ) ;
715+
716+ try {
717+ const response = await fetch ( `${ BASE_URL } /api/magic-mail/send` , {
718+ method : 'POST' ,
719+ headers : { 'Content-Type' : 'application/json' } ,
720+ body : JSON . stringify ( {
721+ to : 'attacker@evil.com' ,
722+ subject : 'Open relay test' ,
723+ text : 'If this arrives, the server is an open relay' ,
724+ } ) ,
725+ } ) ;
726+
727+ if ( response . status === 401 || response . status === 403 ) {
728+ logSuccess ( `Unauthenticated send blocked with ${ response . status } ` ) ;
729+ return true ;
730+ } else if ( response . status === 200 ) {
731+ logError ( 'CRITICAL: Unauthenticated email send succeeded! Server is an OPEN RELAY!' ) ;
732+ return false ;
733+ } else {
734+ logSuccess ( `Unauthenticated send rejected with ${ response . status } ` ) ;
735+ return true ;
736+ }
737+ } catch ( err ) {
738+ logError ( `Unauthenticated send test error: ${ err . message } ` ) ;
739+ return false ;
740+ }
741+ }
742+
743+ /**
744+ * SECURITY TEST 2: Unauthenticated account creation must be blocked
745+ */
746+ async function testUnauthenticatedAccountCreateBlocked ( ) {
747+ logSection ( 'SECURITY 2: Unauthenticated Account Creation Blocked' ) ;
748+
749+ try {
750+ const response = await fetch ( `${ BASE_URL } /magic-mail/accounts` , {
751+ method : 'POST' ,
752+ headers : { 'Content-Type' : 'application/json' } ,
753+ body : JSON . stringify ( {
754+ name : 'Exploit Test Account' ,
755+ provider : 'smtp' ,
756+ config : { host : 'evil.com' , port : 587 , user : 'x' , pass : 'x' } ,
757+ fromEmail : 'exploit@evil.com' ,
758+ } ) ,
759+ } ) ;
760+
761+ if ( response . status === 401 || response . status === 403 ) {
762+ logSuccess ( `Unauthenticated account creation blocked with ${ response . status } ` ) ;
763+ return true ;
764+ } else if ( response . status === 200 || response . status === 201 ) {
765+ logError ( 'CRITICAL: Unauthenticated account creation succeeded!' ) ;
766+ return false ;
767+ } else {
768+ logSuccess ( `Unauthenticated account creation rejected with ${ response . status } ` ) ;
769+ return true ;
770+ }
771+ } catch ( err ) {
772+ logError ( `Unauthenticated account creation test error: ${ err . message } ` ) ;
773+ return false ;
774+ }
775+ }
776+
777+ /**
778+ * SECURITY TEST 3: Path traversal via attachment.path must be stripped
779+ */
780+ async function testPathTraversalBlocked ( ) {
781+ logSection ( 'SECURITY 3: Attachment Path Traversal Blocked' ) ;
782+
783+ try {
784+ const response = await fetch ( `${ BASE_URL } /api/magic-mail/send` , {
785+ method : 'POST' ,
786+ headers : {
787+ 'Authorization' : `Bearer ${ ADMIN_JWT } ` ,
788+ 'Content-Type' : 'application/json' ,
789+ } ,
790+ body : JSON . stringify ( {
791+ to : process . env . TEST_EMAIL || 'test@example.com' ,
792+ subject : 'Path traversal test' ,
793+ text : 'Testing path traversal protection' ,
794+ attachments : [
795+ { path : '/etc/passwd' , filename : 'passwd.txt' } ,
796+ { path : '../../.env' , filename : 'env.txt' } ,
797+ { content : 'Safe content here' , filename : 'safe.txt' } ,
798+ ] ,
799+ } ) ,
800+ } ) ;
801+
802+ const data = await response . json ( ) . catch ( ( ) => ( { } ) ) ;
803+
804+ // The request may succeed (email sent) or fail (no account configured)
805+ // But the key check: did it NOT attach /etc/passwd?
806+ // We verify by checking the response doesn't mention file read errors
807+ // for system files - the path should have been silently stripped
808+ if ( response . status === 500 && data . message && data . message . includes ( '/etc/passwd' ) ) {
809+ logError ( 'CRITICAL: Server attempted to read /etc/passwd!' ) ;
810+ return false ;
811+ }
812+
813+ logSuccess ( 'Attachment path fields are stripped from API requests' ) ;
814+ logInfo ( 'Content-based attachments remain intact, file paths are removed' ) ;
815+ return true ;
816+ } catch ( err ) {
817+ logError ( `Path traversal test error: ${ err . message } ` ) ;
818+ return false ;
819+ }
820+ }
821+
822+ /**
823+ * SECURITY TEST 4: Open redirect in click tracking must be blocked
824+ */
825+ async function testOpenRedirectBlocked ( ) {
826+ logSection ( 'SECURITY 4: Open Redirect Blocked' ) ;
827+
828+ try {
829+ // Try to inject a URL via query parameter
830+ const response = await fetch (
831+ `${ BASE_URL } /api/magic-mail/track/click/fake-id/fake-hash/fake-recipient?url=https://evil-phishing.com` ,
832+ { redirect : 'manual' }
833+ ) ;
834+
835+ if ( response . status === 302 || response . status === 301 ) {
836+ const location = response . headers . get ( 'location' ) ;
837+ if ( location && location . includes ( 'evil-phishing.com' ) ) {
838+ logError ( 'CRITICAL: Open redirect to attacker URL succeeded!' ) ;
839+ return false ;
840+ }
841+ logSuccess ( 'Redirect does not point to attacker URL' ) ;
842+ return true ;
843+ } else if ( response . status === 400 ) {
844+ logSuccess ( `Open redirect blocked with 400 - URL from query param rejected` ) ;
845+ return true ;
846+ } else {
847+ logSuccess ( `Click tracking returned ${ response . status } (no redirect to attacker URL)` ) ;
848+ return true ;
849+ }
850+ } catch ( err ) {
851+ logError ( `Open redirect test error: ${ err . message } ` ) ;
852+ return false ;
853+ }
854+ }
855+
856+ /**
857+ * SECURITY TEST 5: XSS via tracking endpoints
858+ */
859+ async function testXssInTrackingEndpoints ( ) {
860+ logSection ( 'SECURITY 5: XSS Prevention in Tracking' ) ;
861+
862+ try {
863+ // Test tracking pixel with XSS payload in params
864+ const xssPayload = '<script>alert(1)</script>' ;
865+ const encodedPayload = encodeURIComponent ( xssPayload ) ;
866+
867+ const response = await fetch (
868+ `${ BASE_URL } /api/magic-mail/track/open/${ encodedPayload } /${ encodedPayload } `
869+ ) ;
870+
871+ const contentType = response . headers . get ( 'content-type' ) ;
872+
873+ if ( contentType && contentType . includes ( 'image/gif' ) ) {
874+ logSuccess ( 'Tracking pixel returns image/gif, not HTML (XSS not possible)' ) ;
875+ return true ;
876+ } else if ( contentType && contentType . includes ( 'text/html' ) ) {
877+ const body = await response . text ( ) ;
878+ if ( body . includes ( '<script>' ) ) {
879+ logError ( 'CRITICAL: XSS payload reflected in tracking response!' ) ;
880+ return false ;
881+ }
882+ logSuccess ( 'HTML response does not reflect XSS payload' ) ;
883+ return true ;
884+ } else {
885+ logSuccess ( `Tracking returns ${ contentType } - not vulnerable to XSS` ) ;
886+ return true ;
887+ }
888+ } catch ( err ) {
889+ logError ( `XSS test error: ${ err . message } ` ) ;
890+ return false ;
891+ }
892+ }
893+
894+ /**
895+ * SECURITY TEST 6: Error responses must not leak stack traces
896+ */
897+ async function testErrorLeakPrevention ( ) {
898+ logSection ( 'SECURITY 6: Error Message Leak Prevention' ) ;
899+
900+ try {
901+ // Trigger a 500 error with invalid data
902+ const response = await fetch ( `${ BASE_URL } /magic-mail/analytics/emails/nonexistent-id-12345` , {
903+ headers : { 'Authorization' : `Bearer ${ ADMIN_JWT } ` } ,
904+ } ) ;
905+
906+ const data = await response . json ( ) . catch ( ( ) => ( { } ) ) ;
907+ const responseText = JSON . stringify ( data ) ;
908+
909+ // Check for stack trace indicators
910+ const leakPatterns = [
911+ 'node_modules' ,
912+ 'at Object.' ,
913+ 'at Module.' ,
914+ 'at Function.' ,
915+ '.js:' ,
916+ 'Error\n' ,
917+ 'stack' ,
918+ 'node:internal' ,
919+ ] ;
920+
921+ let hasLeak = false ;
922+ for ( const pattern of leakPatterns ) {
923+ if ( responseText . includes ( pattern ) ) {
924+ logError ( `Error response leaks internal info: "${ pattern } " found` ) ;
925+ hasLeak = true ;
926+ break ;
927+ }
928+ }
929+
930+ if ( ! hasLeak ) {
931+ logSuccess ( 'Error responses do not leak stack traces or internal paths' ) ;
932+ return true ;
933+ }
934+ return false ;
935+ } catch ( err ) {
936+ logError ( `Error leak test error: ${ err . message } ` ) ;
937+ return false ;
938+ }
939+ }
940+
941+ /**
942+ * SECURITY TEST 7: License key must be masked in API responses
943+ */
944+ async function testLicenseKeyMasked ( ) {
945+ logSection ( 'SECURITY 7: License Key Masking' ) ;
946+
947+ try {
948+ const response = await fetch ( `${ BASE_URL } /magic-mail/license/status` , {
949+ headers : { 'Authorization' : `Bearer ${ ADMIN_JWT } ` } ,
950+ } ) ;
951+
952+ const data = await response . json ( ) ;
953+ const responseText = JSON . stringify ( data ) ;
954+
955+ // Check if full license key is exposed (keys are typically 20+ chars)
956+ const licenseKey = data . data ?. licenseKey || data . licenseKey ;
957+
958+ if ( licenseKey ) {
959+ if ( licenseKey . includes ( '...' ) ) {
960+ logSuccess ( `License key is masked: ${ licenseKey } ` ) ;
961+ return true ;
962+ } else if ( licenseKey . length > 16 ) {
963+ logError ( `SECURITY: Full license key exposed in API response (${ licenseKey . length } chars)` ) ;
964+ return false ;
965+ } else {
966+ logSuccess ( 'License key appears truncated/masked' ) ;
967+ return true ;
968+ }
969+ } else {
970+ logInfo ( 'No license key in response (may not be activated)' ) ;
971+ return true ;
972+ }
973+ } catch ( err ) {
974+ logError ( `License key mask test error: ${ err . message } ` ) ;
975+ return false ;
976+ }
977+ }
978+
706979/**
707980 * SUMMARY: Print Test Results
708981 */
@@ -810,6 +1083,32 @@ async function runAllTests() {
8101083 await testStrapiEmailService ( ) ;
8111084 await sleep ( 500 ) ;
8121085
1086+ // ============================================================
1087+ // SECURITY TESTS
1088+ // ============================================================
1089+ logCategory ( 'SECURITY TESTS' ) ;
1090+
1091+ await testUnauthenticatedSendBlocked ( ) ;
1092+ await sleep ( 500 ) ;
1093+
1094+ await testUnauthenticatedAccountCreateBlocked ( ) ;
1095+ await sleep ( 500 ) ;
1096+
1097+ await testPathTraversalBlocked ( ) ;
1098+ await sleep ( 500 ) ;
1099+
1100+ await testOpenRedirectBlocked ( ) ;
1101+ await sleep ( 500 ) ;
1102+
1103+ await testXssInTrackingEndpoints ( ) ;
1104+ await sleep ( 500 ) ;
1105+
1106+ await testErrorLeakPrevention ( ) ;
1107+ await sleep ( 500 ) ;
1108+
1109+ await testLicenseKeyMasked ( ) ;
1110+ await sleep ( 500 ) ;
1111+
8131112 // ============================================================
8141113 // FULL TESTS (Only with --full flag)
8151114 // ============================================================
@@ -848,9 +1147,18 @@ Usage:
8481147
8491148Options:
8501149 --quick Run quick tests only (default)
851- --full Run all tests including build & performance
1150+ --full Run all tests including build, performance & security
8521151 --help Show this help message
8531152
1153+ Security Tests (always run):
1154+ - Unauthenticated send blocked (open relay prevention)
1155+ - Unauthenticated account creation blocked
1156+ - Attachment path traversal blocked
1157+ - Open redirect blocked
1158+ - XSS prevention in tracking endpoints
1159+ - Error message leak prevention
1160+ - License key masking
1161+
8541162Environment Variables:
8551163 ADMIN_EMAIL Admin email (required)
8561164 ADMIN_PASSWORD Admin password (required)
0 commit comments