Web apps ship करते समय obfuscation डिफ़ॉल्ट होना चाहिए
आपके ship किए JavaScript में जो कुछ भी है, वह पढ़ा जा सकता है। Secrets, business logic, वह algorithm जिस पर आप निर्भर हैं: कोई भी bundle खोल सकता है, और कहने पर एक frontier LLM उसे लाइन-दर-लाइन समझा देगा। Security scanner ठीक-ठीक दिखाता है कि एक असली production bundle से क्या निकलता है। इसे अपने bundle पर चलाकर देखें कि आप पहले से क्या दे रहे हैं।
उस bundle को obfuscate करने से ठोस, जाँचे जा सकने वाले गुण मिलते हैं:
- आपके app के लिए लिखा patch आपकी releases के बाद टिक नहीं पाता। आपके app को customize या automate करने वाले userscript, Tampermonkey script या extension को किसी स्थिर चीज़ पर टिकना पड़ता है: function का नाम, literal string, या कोई पहचाना जा सकने वाला आकार। Renaming और string encoding इन anchors को हटा देते हैं, और एक नया seed हर build पर output का आकार बदल देता है। लेखक patch को एक release के लिए लिखता है; वह अगली release से match नहीं करता, और जिन users तक उसने उसे पहुँचाया, उनके पास फिर से लिखे जाने तक एक टूटी हुई script रह जाती है।
- आपके code पर find-and-replace को match करने के लिए कुछ नहीं मिलता। Simple text substitution, जो किसी web app को automate करने या बदलने का सबसे सस्ता तरीका है,
isSubscribedऔर"Upgrade to continue"पर काम करता है। इनमें से कोई भी string आपकी ship की गई फ़ाइल में नहीं है। - आपके constants पढ़ने योग्य text नहीं रहते। Literals encode होते हैं, इसलिए किसी key, endpoint, price table या किसी gate के आसपास के text के लिए
grepकुछ नहीं लौटाता, और bundles को बड़े पैमाने पर पढ़ने वाले scanners और scrapers भी कुछ नहीं पाते। - Output आपका program नहीं है। AfterPack logic को समकक्ष लेकिन जानकारी नष्ट करने वाले रूपों में फिर से लिखता है। Artifact पर मेहनत करने वाला reader एक काम करने वाला program recover करता है, वह नहीं जो आपने लिखा था, और वे algorithmic choices जिनकी वजह से आपका program copy करने लायक था, ख़त्म हो चुकी होती हैं।
- लागत बार-बार लगती है। निशाना बनाने के लिए कोई build-invariant संरचना नहीं होती, इसलिए पिछली release के लिए लिखा deobfuscator या patch इस release पर काम नहीं करता, और किसी ने उस पर जो काम पहले कर लिया, वह आगे नहीं जाता।
इनमें से कुछ भी गारंटी नहीं है, और यह server-side authority का विकल्प नहीं है। कोई भी client code कुछ हद तक reverse किया जा सकता है, क्योंकि runtime को उसे चलाना ही पड़ता है। जो बदलता है वह कीमत और उसकी आवृत्ति है: जो भी आपके app को पढ़ना या बदलना चाहता है, वह आपकी ship की हर release पर फिर से इसकी कीमत चुकाता है। इस पेज का बाकी हिस्सा बताता है कि उस सुरक्षा को वहाँ कैसे लगाएँ जहाँ उसकी अहमियत है।
अपनी build rotate करें, attackers से आगे रहें
एक अकेली obfuscated release एक स्थिर निशाना है, जिसके हिसाब से captcha-breaker, cheat extension या scraper पर्याप्त मेहनत से ख़ुद को ढाल सकता है। हर build पर एक नया seed डिफ़ॉल्ट है, इसलिए हर release पहले से ही पिछली से संरचनात्मक रूप से अलग होती है, और जिसने एक के हिसाब से ख़ुद को ढाला, उसे अगली पर फिर से शुरू करना पड़ता है।
अगर आप कम ही ship करते हैं, तो releases के बीच यह बचाव ठहर जाता है। Code बदलावों से स्वतंत्र, schedule पर एक rebuild जोड़ें, ताकि deployed bundle फिर भी rotate होता रहे; Build & CI इसे pipeline में wire करता है।
एक preset चुनें, फिर जो अहम है उसे mark करें
डिफ़ॉल्ट preset light है (complexity target 5): यह string literals को encode करता है, identifiers को rename करता है और syntax को दोबारा लिखता है, और कोई संरचनात्मक layer नहीं जोड़ता। जिस production build को असली सुरक्षा चाहिए, उसके लिए इसे globally बढ़ाएँ:
$ npx afterpack@latest dist/ --preset=hardसीढ़ी है minify (0), light (5), medium (7), hard (12), extreme (25); देखें Presets। --complexity=<number> उस target को override करता है जो preset साथ लाता है: --preset=hard --complexity=40 target 40 पर hard है। इस तरह सेट किया गया मान हर token पर लागू होता है, light पर भी।
फिर उन regions को mark करें जहाँ गलती सबसे महँगी पड़ती है, ताकि भारी treatment वहीं लगे जहाँ वह अपनी size लागत के लायक है।
Directive से किसी region को mark करें
/* @afterpack preset=hard */
function checkLicense(token: string): boolean {
return verifySignature(token, PUBLIC_KEY);
}
/* @afterpack end */
const apiKey = /* @afterpack preset=extreme */ "YOUR_SECRET";
// only the rest of this line is coveredDirectives reference है: grammar, हर key जिसे एक directive सेट कर सकता है, क्या mark करें और क्यों, और उस build में directive का क्या होता है जो उसे लागू नहीं कर सकती। Region overrides Pro builds पर प्रभावी होते हैं।
दो anti-tamper transforms
Anti-tamper preset=hard का हिस्सा नहीं है। यह दो स्पष्ट Pro config flags हैं, दोनों डिफ़ॉल्ट रूप से बंद, और Free पर लागू नहीं होते:
transforms.comparisonHardening.enabledकिसी build-time string literal के साथ strict equality (x === "LIT") को एक one-way digest check में फिर से लिखता है, ताकि literal artifact में बिल्कुल मौजूद न रहे। इसे हर उस check के लिए इस्तेमाल करें जो ऐसे constant से तुलना करता है जिसे आप grep होकर बाहर आने नहीं देना चाहते।transforms.selfIntegrity.enabledobfuscated output पर एक integrity check जोड़ता है। छेड़छाड़ न होने पर व्यवहार पर इसका कोई असर नहीं होता। छेड़छाड़ की गई फ़ाइल सही ढंग से decode नहीं होती। इसे जुड़ने के लिएmediumया उससे ऊपर का code चाहिए, इसलिएlightbuild में यह केवल उसी region के अंदर होता है जिसे उस स्तर तक बढ़ाया गया हो। Engine 0.2.3 से, ऐसे code के बिना build check के बिना ship होने के बजायDIAG_SELF_INTEGRITY_INERTके साथ fail होती है।
दोनों को पूरे program के लिए सेट करें। देखें Configuration।
Domain-locked license या trial को protect करें
AfterPack में कोई built-in domain-lock या expiry transform नहीं है। Check आप ख़ुद लिखें, और AfterPack आपके लिखे JavaScript को protect करता है। Trial expiry के साथ per-domain license check के लिए:
/* @afterpack preset=hard */
if (location.hostname !== "customer-domain.example.com") return;
if (Date.now() > TRIAL_EXPIRES_AT) return;
/* @afterpack end */transforms.comparisonHardening.enabled चालू होने पर location.hostname की तुलना एक digest check बन जाती है, इसलिए अनुमत host artifact में कभी नहीं दिखता। Domain के लिए grep कुछ नहीं पाता। preset=hard आसपास के logic को constants floor चालू रखते हुए complexity target 12 पर रखता है, और एक नया seed हर release पर उसका आकार बदल देता है।
यह check को patch करने की लागत और आवृत्ति बढ़ाता है; यह check को attack surface से नहीं हटाता। Code चलाने वाला एक दृढ़ attacker अब भी तुलना को ढूँढकर patch कर सकता है, लेकिन उसे यह काम हर release पर फिर से करना पड़ता है। इसे trial में रुकावट मानें, और असली revenue को gate करने वाले हर check के साथ server-side entitlement call जोड़ें।
Third-party code पर सुरक्षा बर्बाद न करें
Vendor bundles, जैसे React, Lodash, या कोई CSS-in-JS runtime, को ज़्यादा सुरक्षा की ज़रूरत नहीं। वे पहले से public हैं, आपका bundle फुलाते हैं, और अक्सर runtime reflection इस्तेमाल करते हैं जिसे acknowledge करना पड़ता है।
paths.exclude मेल खाने वाली फ़ाइल को parse होने से पहले ही पूरी तरह छोड़ देता है और उसे byte-दर-byte वैसे ही लौटाता है। Exclude की गई फ़ाइल DIAG_PATH_EXCLUDED के रूप में रिपोर्ट होती है। यह फ़ाइल स्तर पर काम करता है: जब कोई library build आपके code और किसी dependency को एक ही output फ़ाइल में जोड़ देती है, तो exclude करने के लिए अलग फ़ाइल नहीं होती। इसके बजाय identifiers.reserved का उपयोग करें।
AfterPack क्या देखे, यह चुनने के चार तरीके:
- फ़ाइल को exclude करें
paths.excludeसे, उदाहरण के लिए["**/*.min.js", "dist/vendor/**"]।*और?एक path segment के अंदर match करते हैं, पूरा**segment शून्य या अधिक segments से match करता है, और matching case-sensitive है। - CLI को उस directory पर चलाएँ जिसे आप protect करना चाहते हैं। यह build directory में recursively जाता है और मिलने वाली हर
.js/.mjs/.cjsफ़ाइल लेता है, nestednode_modules/(इसेpaths.includeसे शामिल करें) और अपनी.backup.<hash>copies को छोड़कर। उस directory के बाहर का vendor chunk कभी नहीं छुआ जाता। - Vendor code को अपने bundler में एक अलग chunk में बाँटें, फिर AfterPack को ऐसी directory पर चलाएँ जिसमें वह न हो। अगर आप लागत स्वीकार करना पसंद करें, तो vendor chunk भी ठीक से obfuscate हो जाता है।
- Regions को
skipसे mark करें जहाँ किसी bundled dependency का code आपके code वाली फ़ाइल में ही है और उसे पहचानने योग्य रहना है।
आपके imports फिर भी resolve होते हैं।
AfterPack ES2022 private class fields और methods को पूरी तरह support करता है: #field और #method() सही ढंग से parse होते हैं और बाकी सब की तरह rename और protect होते हैं।
जो पढ़ने योग्य रहना चाहिए, उसे बचाएँ
Runtime के काम करने के लिए कुछ code को अपने मूल नाम रखने पड़ते हैं। Framework presets आम मामलों को कवर करते हैं। बाकी के लिए, सीमित दायरे में बचाएँ। Renaming को कभी globally बंद न करें, वरना आप अपने पूरे bundle में सुरक्षा की सबसे सस्ती layer गँवा देते हैं।
| स्थिति | क्या करें |
|---|---|
| Public exports जिन्हें callers नाम से import करते हैं | Export के चारों ओर /* @afterpack skip */ … /* @afterpack end */, या नामों को identifiers.reserved से pin करें |
| Stack traces जिन्हें आप सच में debug करते हैं | Deploy tree से बाहर रखे source maps से symbolicate करें। identifiers.rename=false पूरे bundle में हर identifier को ज्यों का त्यों रखता है |
| Reflection-आधारित DI (Angular, NestJS, Inversify) | किसी custom pattern को acknowledge करने के लिए config में reflection.allow; acknowledge न किया गया pattern एक build error है |
instanceof / class-name के आधार पर switching | Class names के साथ identifiers.reserved, या declaration के चारों ओर /* @afterpack skip */ |
| आपके अपने code वाली फ़ाइल में ही bundled third-party API symbols (जैसे Rollup/Vite library build जो किसी dependency को inline करती है) | identifiers.reserved, पूरी build के लिए सादे नाम के साथ या मेल खाने वाली फ़ाइलों के लिए { glob, names } entry के साथ |
| ऐसी script जिसमें बाहर से कुछ call नहीं करता | identifiers.globals.rename: true। उस ambient global surface की renaming चालू करता है जिसे engine वरना बचाए रखता है। डिफ़ॉल्ट रूप से बंद, और केवल तब सुरक्षित जब बाहर का कुछ भी उन नामों को नहीं पढ़ता |
identifiers.reserved केवल आपके सूचीबद्ध नामों को बचाता है, वे जहाँ भी दिखें, किसी भी renaming से पहले source में लिखे नाम से match करके: exact, पूरा identifier, case-sensitive। Free पर उपलब्ध। इसका उपयोग तब करें जब आप पूरा region mark नहीं कर सकते, क्योंकि आपका code और कोई vendored dependency एक ही फ़ाइल में हैं।
नामों को केवल एक जगह बचाए रखने के लिए identifiers.reserved या /* @afterpack skip */ का उपयोग करें; देखें आप क्या सेट कर सकते हैं।
हर ऐसा बचाव उन लाइनों पर सुरक्षा घटाता है। इसे केवल वहीं लागू करें जहाँ runtime को इसकी ज़रूरत है।
Seed strategy
Seed obfuscated output का आकार तय करता है। हर build पर random seed डिफ़ॉल्ट है। Production के लिए इसे ऐसा ही रहने दें: हर release पर बदलने वाली build मुख्य बचाव है।
Seed को ठीक एक जगह pin करें: CI में, जब cache hits या golden-snapshot tests के लिए आपको reproducible output चाहिए।
$ npx afterpack@latest dist/ --seed=git--seed=git seed को git rev-parse HEAD से निकालता है। Repository न होने पर यह build को विफल करने के बजाय एक नए random seed पर लौट आता है और एक notice print करता है। इससे output हर commit के लिए deterministic रहता है, फिर भी हर release पर rotate होता है।
Seed का एक environment variable भी है, AFTERPACK_seed, जिसके मान --seed वाले ही हैं। Multi-bundle builds अपने-आप एक seed साझा करती हैं, एक process के अंदर भी और इस variable के ज़रिए processes के पार भी। इसे ख़ुद केवल किसी खास build को reproduce करने के लिए सेट करें; स्पष्ट --seed या seed option फिर भी इस पर भारी पड़ता है।
Production को एक बार build करें और वही bytes qa → preprod → prod तक promote करें। Dev builds un-obfuscated होती हैं, इसलिए रोज़मर्रा का development अप्रभावित रहता है।
Production में constant seed ship न करें। इससे releases के बीच output का आकार बदलना बंद हो जाता है।
Artifacts को deploy से बाहर रखें
AfterPack तीन तरह के local artifact लिखता है। हर एक उस bundle से ज़्यादा संवेदनशील है जिसका वह वर्णन करता है।
- Protection Map (
protectionMap.html) में आपका मूल source embed होता है। यह हमेशा project root की gitignored.afterpack/directory में लिखा जाता है, serve होने वाली build के साथ कभी नहीं, और इसके चालू होने पर production build चेतावनी देती है। इसे--protectionMap.enabled=falseसे बंद करें। - आपकी मूल फ़ाइलों का backup। किसी फ़ाइल को उसी जगह फिर से लिखने से पहले CLI मूल फ़ाइल को
.afterpack/backup/में copy करता है, साथ में एक manifest कि उसने हर फ़ाइल को किसमें obfuscate किया। वह directory ज्यों का त्यों आपका source है, build machine पर ही रहती है, औरnpx afterpack@latest restoreमूल फ़ाइलें वापस रख देता है।--build.backup=falseसे इसे बंद करें। Framework plugin डिफ़ॉल्ट रूप से कोई copy नहीं लिखता; वहाँbuild.backupकोtrueकरने पर इसके बजाय output के बगल में एक.backup.<hash>फ़ाइल लिखी जाती है, जहाँ वह build के साथ ship हो जाएगी। - Source maps obfuscated output को सीधे मूल source तक वापस जोड़ते हैं।
.mapफ़ाइल production में डिफ़ॉल्ट रूप से बंद रहती है, और//# sourceMappingURL=comment भी production में डिफ़ॉल्ट रूप से बंद रहता है।
Plugins और CLI .afterpack/ के अंदर एक .gitignore रखकर उसे git से बाहर रखते हैं, आपकी .gitignore में कभी बदलाव नहीं करते, और किसी artifact के serve होने वाले path के नीचे पहुँचने पर चेतावनी देते हैं। Build के बगल में मौजूद .map या .backup.<hash> फ़ाइल को git से बाहर रखना आपका काम है।
Source map या backup में आपका पूरा source होता है। इनमें से किसी को भी कभी deploy न करें।
@afterpack/next build के बाद serve होने वाले tree से हर .js.map भी हटा देता है।
Map publish किए बिना production traces debug करें
Production build न कोई .map लिखती है, न //# sourceMappingURL= comment, जो सही डिफ़ॉल्ट है, और इससे आपको stack traces हाथ से symbolicate करने पड़ते हैं। अगर आपकी team को production से असली traces चाहिए, तो map को छोड़ने के बजाय उसे build करें और private रखें।
Build के लिए map चालू करें और pointer बंद रखें, ताकि ship हुए JavaScript में कुछ भी यह न बताए कि कोई map मौजूद है:
{
"sourceMap": { "enabled": true, "emitUrl": false, "sourcesContent": true }
}फिर .map फ़ाइलों को deployed directory से बाहर रखें: उन्हें अपने error tracker पर upload करें या CI artifact के रूप में रखें, और वहीं symbolicate करें। अगर आपका deploy पूरी build directory copy करता है, तो edge पर भी इन फ़ाइलों को deny करें, ताकि किसी के लिए request आने पर वह कभी न लौटे।
location ~ \.map$ {
return 404;
}इसी नियम का समकक्ष हर जगह है: *.map से match करने वाला CDN या WAF नियम, proxy deny, या ऐसा storage bucket जिसमें ये फ़ाइलें कभी पहुँचती ही नहीं। अपने network के बाहर से curl -I https://yoursite.example.com/assets/app.js.map से इसे जाँचें, 404 की अपेक्षा के साथ, और npx afterpack@latest audit से, जो पहुँच में आने वाले map को एक finding के रूप में रिपोर्ट करता है।
Edge नियम उन फ़ाइलों को protect करता है जो server पर हैं। जो कुछ भी उसे bypass करता है, जैसे गलत configure किया गया route, bucket का सीधा URL, या preview deployment, वह map को आपके मूल source समेत serve कर देता है। Maps को deploy न करना ही वह तरीका है जिसे गलत configure नहीं किया जा सकता।
अगर आपने गलती से कोई secret ship कर दिया
आपके bundle तक पहुँची API key, token या password एक चालू incident है। AfterPack यह बदलता है कि reader को ship हुई फ़ाइल में क्या मिलता है। यह इस तथ्य को नहीं बदलता कि credential बाहर जा चुका है।
AfterPack इसके साथ क्या करता है। डिफ़ॉल्ट preset पर string literals encode होते हैं, इसलिए किसी literal में रखी key आपकी deploy की गई फ़ाइल में पढ़ने योग्य नहीं होती। जो थोड़े-से literals हमेशा पढ़ने योग्य रहते हैं, उनकी सूची Presets में है। Bundle में key, या उसके आसपास के text को खोजने पर कुछ नहीं मिलता। Automated secret scanners और bundle scrapers को भी कुछ नहीं मिलता, और ज़्यादातर leaked keys इन्हीं से पकड़ी जाती हैं।
AfterPack क्या नहीं करता। Credential अब भी build में है, और आपका code अब भी उसका उपयोग करता है। अपने नियंत्रण वाले browser में आपका application चलाने वाला कोई भी, code के उसका उपयोग करने के पल value तक पहुँच सकता है। Encoding आपको जवाब देने का समय देती है। यह client-side secret को सुरक्षित नहीं बनाती।
क्या करें, इसी क्रम में:
- Key को rotate करें। जिस पल build बाहर गई, उसी पल से इसे compromised मानें। यही वह कदम है जो incident को बंद करता है।
- Authority को server-side ले जाएँ। Credential आपका अपना backend रखे और वही तय करे कि किसी caller को नतीजा मिलना चाहिए या नहीं। जो key browser को कभी मिलती ही नहीं, वह leak नहीं हो सकती।
- जाँचें कि live क्या है। अपनी deployed site पर
npx afterpack@latest auditचलाएँ। यह site के असल में serve किए JavaScript के लिए exposed secrets, leaked source maps और protection level रिपोर्ट करता है, ताकि आपको पता चले कि एक reader अभी production से क्या निकाल सकता है। - Rebuild और redeploy करें, ताकि पुरानी value वाला bundle serve होना बंद हो जाए।
जो credential ship हो चुका है, उसे rotate करना ही होगा, चाहे उसे किसी भी preset ने build किया हो।
Obfuscated build को test करें
अपने integration tests हमेशा obfuscated build पर चलाएँ। Obfuscation से पहले की build को test करने से आपको कुछ पता नहीं चलता कि क्या ship होता है।
जो test सादी build पर pass हुआ और obfuscated build पर fail हुआ, वह या तो बचाव में किसी कमी की ओर इशारा करता है (runtime को जिस नाम की अपेक्षा थी वह rename हो गया) या किसी अपरिचित reflection pattern की ओर। Diagnostic आमतौर पर उस config key का नाम बताता है जो इसे ठीक करती है। संक्षिप्त सारांश के बजाय हर diagnostic पूरा देखने के लिए --diagnostics.level=all के साथ चलाएँ।
जो कुछ भी आपकी configure की गई सुरक्षा से कमज़ोर सुरक्षा ship करता, वह इसके बजाय build को रोक देता है, जिसमें वह फ़ाइल भी शामिल है जिसे engine obfuscate नहीं कर सकता और वह Pro build भी जो cloud तक नहीं पहुँच सकती। Exit contract और उसके पीछे का हर code Diagnostics में है।
आगे
- Build & CI: वही bytes promote करने का lifecycle, artifact की साफ़-सफ़ाई, और समय-समय पर rebuilds।
- Threat model: attackers की वह सीढ़ी जिसके सापेक्ष ये directives तय किए गए हैं।
- Configuration: हर config key एक जगह।
- Directives: ऊपर की तालिका के पीछे का पूरा region-marker grammar।
- Presets: पाँच-स्तरीय सीढ़ी जिसे ये directives हर region के लिए override करते हैं।
- Protection Map: जाँचें कि आपके mark किए regions पर वास्तव में कौन-सी class लगी।
- Dashboard में builds: आपकी Pro Cloud builds के लिए, हर build के वही आँकड़े।