[{"data":1,"prerenderedAt":696},["ShallowReactive",2],{"post-server-side-template-injection":3},{"id":4,"title":5,"body":6,"description":682,"extension":683,"meta":684,"navigation":633,"path":692,"seo":693,"stem":694,"__hash__":695,"dateDisplay":690,"image":691,"tags":686,"badge":685,"readingTime":153},"posts\u002Fposts\u002FServer-Side Template Injection.md","Server-Side Template Injection (SSTI)",{"type":7,"value":8,"toc":676},"minimark",[9,18,21,26,43,47,52,58,63,175,180,188,195,201,205,210,215,221,224,229,233,238,243,249,252,257,261,266,271,277,280,284,337,341,362,366,380,384,445,449,454,516,521,618,622,672],[10,11,12,13,17],"p",{},"Rồi 👍 mình viết ",[14,15,16],"strong",{},"SSTI (Server-Side Template Injection)"," theo cùng cấu trúc bạn vừa dùng cho SSRF nhé — đầy đủ từ định nghĩa, phân loại, payload, hậu quả đến phòng ngừa.",[19,20],"hr",{},[22,23,25],"h1",{"id":24},"_1-ssti-là-gì-tóm-tắt","1) SSTI là gì — tóm tắt",[10,27,28,30,31,34,35,38,39,42],{},[14,29,5],{}," là lỗi khi ứng dụng render ",[14,32,33],{},"template"," trên server nhưng lại cho phép dữ liệu từ user trở thành ",[14,36,37],{},"một phần template"," . Attacker có thể chèn biểu thức template để engine thực thi → từ leak data đến ",[14,40,41],{},"RCE"," (nếu engine cho phép truy cập object hệ thống).",[22,44,46],{"id":45},"_2-phân-loại-ssti-ví-dụ-cho-mỗi-loại","2) Phân loại SSTI & ví dụ cho mỗi loại",[48,49,51],"h2",{"id":50},"a-basic-ssti-in-band","A. Basic SSTI (In-band)",[10,53,54,57],{},[14,55,56],{},"Mô tả:"," attacker chèn biểu thức đơn giản để xác nhận engine có evaluate.",[10,59,60],{},[14,61,62],{},"Ví dụ vulnerable code (Node.js EJS pseudo):",[64,65,70],"pre",{"className":66,"code":67,"language":68,"meta":69,"style":69},"language-js shiki shiki-themes github-light github-dark","app.get('\u002Fhello', (req, res) => {\n  const tmpl = \"\u003Ch1>Hello \" + req.query.name + \"\u003C\u002Fh1>\"; \u002F\u002F user input nối trực tiếp\n  res.send(ejs.render(tmpl));\n});\n","js","",[71,72,73,116,151,169],"code",{"__ignoreMap":69},[74,75,78,82,86,89,93,96,100,103,106,109,113],"span",{"class":76,"line":77},"line",1,[74,79,81],{"class":80},"sVt8B","app.",[74,83,85],{"class":84},"sScJk","get",[74,87,88],{"class":80},"(",[74,90,92],{"class":91},"sZZnC","'\u002Fhello'",[74,94,95],{"class":80},", (",[74,97,99],{"class":98},"s4XuR","req",[74,101,102],{"class":80},", ",[74,104,105],{"class":98},"res",[74,107,108],{"class":80},") ",[74,110,112],{"class":111},"szBVR","=>",[74,114,115],{"class":80}," {\n",[74,117,119,122,126,129,132,135,138,141,144,147],{"class":76,"line":118},2,[74,120,121],{"class":111},"  const",[74,123,125],{"class":124},"sj4cs"," tmpl",[74,127,128],{"class":111}," =",[74,130,131],{"class":91}," \"\u003Ch1>Hello \"",[74,133,134],{"class":111}," +",[74,136,137],{"class":80}," req.query.name ",[74,139,140],{"class":111},"+",[74,142,143],{"class":91}," \"\u003C\u002Fh1>\"",[74,145,146],{"class":80},"; ",[74,148,150],{"class":149},"sJ8bj","\u002F\u002F user input nối trực tiếp\n",[74,152,154,157,160,163,166],{"class":76,"line":153},3,[74,155,156],{"class":80},"  res.",[74,158,159],{"class":84},"send",[74,161,162],{"class":80},"(ejs.",[74,164,165],{"class":84},"render",[74,167,168],{"class":80},"(tmpl));\n",[74,170,172],{"class":76,"line":171},4,[74,173,174],{"class":80},"});\n",[10,176,177],{},[14,178,179],{},"Payload (lab):",[64,181,186],{"className":182,"code":184,"language":185},[183],"language-text","\u002Fhello?name=\u003C%= 7*7 %>\n","text",[71,187,184],{"__ignoreMap":69},[10,189,190,191,194],{},"Nếu output hiển thị ",[71,192,193],{},"49"," → chứng tỏ SSTI.",[10,196,197,200],{},[14,198,199],{},"Hậu quả:"," chứng minh khả năng thực thi expression, mở đường leo thang.",[48,202,204],{"id":203},"b-variableobject-access","B. Variable\u002FObject Access",[10,206,207,209],{},[14,208,56],{}," attacker từ input truy xuất biến trong context hoặc class nội bộ.",[10,211,212],{},[14,213,214],{},"Ví dụ payload (Jinja2, lab):",[64,216,219],{"className":217,"code":218,"language":185},[183],"{{ config.items() }}\n",[71,220,218],{"__ignoreMap":69},[10,222,223],{},"→ đọc config Flask app.",[10,225,226,228],{},[14,227,199],{}," lộ secrets, config, env vars.",[48,230,232],{"id":231},"c-sandbox-escape-rce","C. Sandbox Escape \u002F RCE",[10,234,235,237],{},[14,236,56],{}," nếu engine cho phép truy cập object chain, attacker leo tới OS-level calls.",[10,239,240],{},[14,241,242],{},"Ví dụ lab payload (Jinja2):",[64,244,247],{"className":245,"code":246,"language":185},[183],"{{ ''.__class__.__mro__[1].__subclasses__() }}\n",[71,248,246],{"__ignoreMap":69},[10,250,251],{},"→ dò các class, từ đó tìm subprocess\u002Fos → thực thi lệnh.",[10,253,254,256],{},[14,255,199],{}," Remote Code Execution.",[48,258,260],{"id":259},"d-blind-ssti","D. Blind SSTI",[10,262,263,265],{},[14,264,56],{}," response không hiển thị trực tiếp kết quả expression, nhưng attacker suy ra qua side-channel (thời gian, lỗi).",[10,267,268],{},[14,269,270],{},"Payload test (lab):",[64,272,275],{"className":273,"code":274,"language":185},[183],"{{ 7\u002F0 }}\n",[71,276,274],{"__ignoreMap":69},[10,278,279],{},"Nếu server trả error stacktrace liên quan template engine → SSTI blind.",[22,281,283],{"id":282},"_3-payload-mẫu-lab-only","3) Payload mẫu (lab-only)",[285,286,287,305,316,327],"ul",{},[288,289,290,291],"li",{},"Detect:",[285,292,293,299],{},[288,294,295,298],{},[71,296,297],{},"{{7*7}}"," (Jinja2\u002FTwig)",[288,300,301,304],{},[71,302,303],{},"\u003C%= 7*7 %>"," (ERB\u002FEJS)",[288,306,307,308],{},"Access variable:",[285,309,310],{},[288,311,312,315],{},[71,313,314],{},"{{ request.headers }}"," (Flask\u002FJinja2)",[288,317,318,319],{},"Blind\u002Ferror-based:",[285,320,321],{},[288,322,323,326],{},[71,324,325],{},"{{ 1\u002F0 }}"," → error",[288,328,329,330],{},"Sandbox escape (lab):",[285,331,332],{},[288,333,334],{},[71,335,336],{},"{{ ''.__class__.__mro__[1].__subclasses__() }}",[22,338,340],{"id":339},"_4-cách-phát-hiện-kiểm-thử-ssti","4) Cách phát hiện \u002F kiểm thử SSTI",[285,342,343,346,352,359],{},[288,344,345],{},"Tìm endpoint render output từ input (profile name, message template, error page).",[288,347,348,349,351],{},"Thử payload benign (",[71,350,297],{},") → nếu evaluate → SSTI.",[288,353,354,355,358],{},"Thử gây error (",[71,356,357],{},"{{1\u002F0}}",") → nếu stacktrace của template engine lộ ra.",[288,360,361],{},"Kiểm tra logs\u002Ferror server để xác nhận.",[22,363,365],{"id":364},"_5-hậu-quả-thực-tế","5) Hậu quả thực tế",[285,367,368,371,374,377],{},[288,369,370],{},"Data exposure (config, env vars).",[288,372,373],{},"Template tampering (modify view logic).",[288,375,376],{},"RCE (truy cập object chain → gọi hệ thống).",[288,378,379],{},"Toàn quyền server, lateral movement.",[22,381,383],{"id":382},"_6-phòng-ngừa-best-practices","6) Phòng ngừa — best practices",[385,386,387,393,403,413,419,425,431],"ol",{},[288,388,389,392],{},[14,390,391],{},"Không render template từ input user"," (chỉ render template file sẵn có).",[288,394,395,398,399,402],{},[14,396,397],{},"Escape \u002F autoescape",": bật autoescape (Jinja2 ",[71,400,401],{},"select_autoescape",", Twig autoescape).",[288,404,405,408,409,412],{},[14,406,407],{},"Whitelist template features",": tắt eval, ",[71,410,411],{},"$where",", helpers nguy hiểm.",[288,414,415,418],{},[14,416,417],{},"Không bind object nhạy cảm"," (os, db, config) vào template context.",[288,420,421,424],{},[14,422,423],{},"Sandbox (nếu thật cần)",": dùng sandboxed environment, nhưng vẫn dễ bypass.",[288,426,427,430],{},[14,428,429],{},"Validate input",": chỉ inject dữ liệu primitive (string\u002Fnumber), không object\u002Fcode.",[288,432,433,436,437,440,441,444],{},[14,434,435],{},"Code review \u002F pentest",": kiểm tra chỗ ",[71,438,439],{},"render(user_input)"," hoặc ",[71,442,443],{},"compile()"," từ input.",[22,446,448],{"id":447},"_7-ví-dụ-sửa-code-nodejs-với-handlebars","7) Ví dụ sửa code (Node.js với Handlebars)",[10,450,451],{},[14,452,453],{},"Vulnerable:",[64,455,457],{"className":66,"code":456,"language":68,"meta":69,"style":69},"app.get('\u002Fhello', (req, res) => {\n  const tmpl = req.query.tmpl; \u002F\u002F user cung cấp template\n  res.send(Handlebars.compile(tmpl)({}));\n});\n",[71,458,459,483,497,512],{"__ignoreMap":69},[74,460,461,463,465,467,469,471,473,475,477,479,481],{"class":76,"line":77},[74,462,81],{"class":80},[74,464,85],{"class":84},[74,466,88],{"class":80},[74,468,92],{"class":91},[74,470,95],{"class":80},[74,472,99],{"class":98},[74,474,102],{"class":80},[74,476,105],{"class":98},[74,478,108],{"class":80},[74,480,112],{"class":111},[74,482,115],{"class":80},[74,484,485,487,489,491,494],{"class":76,"line":118},[74,486,121],{"class":111},[74,488,125],{"class":124},[74,490,128],{"class":111},[74,492,493],{"class":80}," req.query.tmpl; ",[74,495,496],{"class":149},"\u002F\u002F user cung cấp template\n",[74,498,499,501,503,506,509],{"class":76,"line":153},[74,500,156],{"class":80},[74,502,159],{"class":84},[74,504,505],{"class":80},"(Handlebars.",[74,507,508],{"class":84},"compile",[74,510,511],{"class":80},"(tmpl)({}));\n",[74,513,514],{"class":76,"line":171},[74,515,174],{"class":80},[10,517,518],{},[14,519,520],{},"Safe:",[64,522,524],{"className":66,"code":523,"language":68,"meta":69,"style":69},"const template = Handlebars.compile(\"Hello, {{name}}!\");\napp.get('\u002Fhello', (req, res) => {\n  const name = String(req.query.name || 'guest'); \u002F\u002F ép kiểu\n  res.send(template({ name }));\n});\n",[71,525,526,549,573,600,613],{"__ignoreMap":69},[74,527,528,531,534,536,539,541,543,546],{"class":76,"line":77},[74,529,530],{"class":111},"const",[74,532,533],{"class":124}," template",[74,535,128],{"class":111},[74,537,538],{"class":80}," Handlebars.",[74,540,508],{"class":84},[74,542,88],{"class":80},[74,544,545],{"class":91},"\"Hello, {{name}}!\"",[74,547,548],{"class":80},");\n",[74,550,551,553,555,557,559,561,563,565,567,569,571],{"class":76,"line":118},[74,552,81],{"class":80},[74,554,85],{"class":84},[74,556,88],{"class":80},[74,558,92],{"class":91},[74,560,95],{"class":80},[74,562,99],{"class":98},[74,564,102],{"class":80},[74,566,105],{"class":98},[74,568,108],{"class":80},[74,570,112],{"class":111},[74,572,115],{"class":80},[74,574,575,577,580,582,585,588,591,594,597],{"class":76,"line":153},[74,576,121],{"class":111},[74,578,579],{"class":124}," name",[74,581,128],{"class":111},[74,583,584],{"class":84}," String",[74,586,587],{"class":80},"(req.query.name ",[74,589,590],{"class":111},"||",[74,592,593],{"class":91}," 'guest'",[74,595,596],{"class":80},"); ",[74,598,599],{"class":149},"\u002F\u002F ép kiểu\n",[74,601,602,604,606,608,610],{"class":76,"line":171},[74,603,156],{"class":80},[74,605,159],{"class":84},[74,607,88],{"class":80},[74,609,33],{"class":84},[74,611,612],{"class":80},"({ name }));\n",[74,614,616],{"class":76,"line":615},5,[74,617,174],{"class":80},[22,619,621],{"id":620},"_8-checklist-kiểm-thử-remediation","8) Checklist kiểm thử & remediation",[285,623,626,636,648,657,666],{"className":624},[625],"contains-task-list",[288,627,630,635],{"className":628},[629],"task-list-item",[631,632],"input",{"disabled":633,"type":634},true,"checkbox"," Xác định endpoint nào render template từ input.",[288,637,639,641,642,102,644,647],{"className":638},[629],[631,640],{"disabled":633,"type":634}," Test ",[71,643,297],{},[71,645,646],{},"\u003C%=7*7%>"," để detect.",[288,649,651,653,654,656],{"className":650},[629],[631,652],{"disabled":633,"type":634}," Test error (",[71,655,357],{},").",[288,658,660,662,663,656],{"className":659},[629],[631,661],{"disabled":633,"type":634}," Kiểm tra context leak (",[71,664,665],{},"{{config}}",[288,667,669,671],{"className":668},[629],[631,670],{"disabled":633,"type":634}," Remediate: không render user template, enable autoescape, remove dangerous helpers, sandbox\u002Fvalidate.",[673,674,675],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":69,"searchDepth":118,"depth":118,"links":677},[678,679,680,681],{"id":50,"depth":118,"text":51},{"id":203,"depth":118,"text":204},{"id":231,"depth":118,"text":232},{"id":259,"depth":118,"text":260},"Lỗ hổng khi server render template từ input người dùng (Jinja2\u002FTwig\u002FERB, ...), cho phép chèn biểu thức\u002Fobject trong template để leak thông tin hoặc thực thi mã (RCE). Bao gồm payload detect phổ biến, dấu hiệu nhận biết và biện pháp phòng ngừa (không render template do user cung cấp, bật autoescape, sandbox, validate\u002Fwhitelist input).","md",{"badge":685,"tags":686,"date":689,"dateDisplay":690,"image":691},"Web Security",[687,688],"Web","SSTI","2025-06-20","Jun 20, 2025","\u002Fthumbnails\u002Fowasp-top-ten-1000.png","\u002Fposts\u002Fserver-side-template-injection",{"title":5,"description":682},"posts\u002FServer-Side Template Injection","uLgjezndO3ZXbyjHQeXkXx8ajJyLPZ3eWhd211EtAo8",1787385494289]