@@ -91,6 +91,63 @@ $(function() {
9191 } ) ;
9292 }
9393
94+ /* Sanitize response message HTML: keep allowed tags, escape everything else.
95+ * Must mirror the server-side sanitize_response_message() helper in feedback/utils.py. */
96+ const _ALLOWED_RESPONSE_TAGS = new Set ( [ 'B' , 'I' , 'U' , 'STRONG' , 'EM' , 'CODE' , 'A' , 'BR' ] ) ;
97+ const _ALLOWED_RESPONSE_ATTRS = new Map ( [ [ 'A' , new Set ( [ 'href' , 'title' ] ) ] ] ) ;
98+ const _SAFE_URL_SCHEMES = new Set ( [ 'http:' , 'https:' , 'mailto:' ] ) ;
99+
100+ function _sanitizeResponseNode ( source , target ) {
101+ for ( const child of source . childNodes ) {
102+ if ( child . nodeType === Node . TEXT_NODE ) {
103+ target . appendChild ( document . createTextNode ( child . data ) ) ;
104+ } else if ( child . nodeType === Node . ELEMENT_NODE ) {
105+ const tag = child . tagName ; // uppercase in HTML documents
106+ if ( _ALLOWED_RESPONSE_TAGS . has ( tag ) ) {
107+ const el = document . createElement ( tag . toLowerCase ( ) ) ;
108+ const allowedAttrs = _ALLOWED_RESPONSE_ATTRS . get ( tag ) || new Set ( ) ;
109+ for ( const attrName of allowedAttrs ) {
110+ const val = child . getAttribute ( attrName ) ;
111+ if ( val === null ) continue ;
112+ if ( attrName === 'href' ) {
113+ try {
114+ const parsed = new URL ( val , window . location . href ) ;
115+ if ( ! _SAFE_URL_SCHEMES . has ( parsed . protocol ) ) continue ;
116+ } catch ( _ ) { continue ; }
117+ }
118+ el . setAttribute ( attrName , val ) ;
119+ }
120+ _sanitizeResponseNode ( child , el ) ;
121+ target . appendChild ( el ) ;
122+ } else {
123+ // Render the unknown tag as literal text so it is visible
124+ let openTag = '<' + child . tagName . toLowerCase ( ) ;
125+ for ( const attr of child . attributes ) {
126+ openTag += ' ' + attr . name + '="' + attr . value
127+ . replace ( / & / g, '&' ) . replace ( / " / g, '"' ) + '"' ;
128+ }
129+ openTag += '>' ;
130+ target . appendChild ( document . createTextNode ( openTag ) ) ;
131+ // Still process children rather than discarding them
132+ _sanitizeResponseNode ( child , target ) ;
133+ if ( child . childNodes . length > 0 ) {
134+ target . appendChild ( document . createTextNode ( '</' + child . tagName . toLowerCase ( ) + '>' ) ) ;
135+ }
136+ }
137+ }
138+ }
139+ }
140+
141+ function sanitizeResponseHTML ( text ) {
142+ const tmp = document . createElement ( 'div' ) ;
143+ tmp . innerHTML = text ;
144+ const out = document . createElement ( 'div' ) ;
145+ _sanitizeResponseNode ( tmp , out ) ;
146+ return out . innerHTML ; // safe HTML string with disallowed tags escaped
147+ }
148+ // Expose globally so dynamic_forms.js can look it up via data-html-sanitizer
149+ window . sanitizeResponseHTML = sanitizeResponseHTML ;
150+
94151 /* Buttons for toggling preview state */
95152 let sStart , sEnd ;
96153 function on_preview_button ( e ) {
@@ -101,7 +158,10 @@ $(function() {
101158 sStart = this . selectionStart ;
102159 sEnd = this . selectionEnd ;
103160 ta . hide ( ) ;
104- ta . after ( '<span class="textarea preview">' + ta . val ( ) + '</span>' ) ;
161+ const previewSpan = document . createElement ( 'span' ) ;
162+ previewSpan . className = 'textarea preview' ;
163+ previewSpan . innerHTML = sanitizeResponseHTML ( ta . val ( ) ) ;
164+ ta . after ( previewSpan ) ;
105165 } ) ;
106166 me . hide ( ) ;
107167 me . siblings ( '.unpreview-button' ) . show ( ) . focus ( ) ;
@@ -717,11 +777,11 @@ async function studentDiscussionPreview(btn) {
717777 /* clean up response message, inject text into div rather than embedding form */
718778 const response_msgs = contentDiv . querySelectorAll ( '.response-message' ) ;
719779 for ( const rsp_msg of response_msgs ) {
720- const text_content = rsp_msg . querySelector ( 'textarea' ) . innerText ;
780+ const text_content = rsp_msg . querySelector ( 'textarea' ) . value ;
721781 if ( text_content ) {
722782 const textDiv = document . createElement ( 'div' ) ;
723783 textDiv . className = 'display-response' ;
724- textDiv . innerText = text_content ;
784+ textDiv . innerHTML = sanitizeResponseHTML ( text_content ) ;
725785 rsp_msg . firstElementChild . replaceWith ( textDiv ) ; // replace form with text div
726786 } else { // no text content, so don't display anything
727787 rsp_msg . remove ( ) ;
0 commit comments