-
Notifications
You must be signed in to change notification settings - Fork 4.2k
fix: enhance PDF viewer security by removing file parameters and validating URLs #37934
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -86,7 +86,7 @@ def pdf_index(request, course_id, book_index, chapter=None, page=None): | |
| raise Http404(f"Invalid book index value: {book_index}") | ||
| textbook = course.pdf_textbooks[book_index] | ||
|
|
||
| viewer_params = '&file=' | ||
| viewer_params = '' | ||
| current_url = '' | ||
|
|
||
| if 'url' in textbook: | ||
|
|
@@ -99,6 +99,9 @@ def pdf_index(request, course_id, book_index, chapter=None, page=None): | |
| if 'chapters' in textbook: | ||
| for entry in textbook['chapters']: | ||
| entry['url'] = remap_static_url(entry['url'], course) | ||
| # Security: Validate chapter URL doesn't contain dangerous schemes | ||
| if entry['url'].lower().startswith(('javascript:', 'data:', 'vbscript:', 'file:')): | ||
| entry['url'] = '' # Sanitize dangerous URLs | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it makes sense to atleast log a message in the case where the URL might be malicious so that operators can notice and potentially react to the issue. The message probably needs to contain the malicious URL and the course it's in in order to respond to the issue. |
||
| if chapter is not None and int(chapter) <= (len(textbook['chapters'])): | ||
| current_chapter = textbook['chapters'][int(chapter) - 1] | ||
| else: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| <%page expression_filter="h"/> | ||
| <!DOCTYPE html> | ||
| <!DOCTYPE html> | ||
| <%namespace name='static' file='static_content.html'/> | ||
| <%! | ||
| from openedx.core.djangolib.js_utils import ( | ||
|
|
@@ -46,10 +46,44 @@ | |
| PDFJS.workerSrc = "${static.url('js/vendor/pdfjs/pdf.worker.js') | n, js_escaped_string}"; | ||
| PDFJS.disableWorker = true; | ||
| PDFJS.cMapUrl = "${static.url('css/vendor/pdfjs/cmaps/') | n, js_escaped_string}"; | ||
| PDF_URL = '${current_url | n, js_escaped_string}'; | ||
|
|
||
| var PDF_URL = '${current_url | n, js_escaped_string}'; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you explain what the goal of this function is? It looks like previously we were just letting the pdf.js library load the PDF from the |
||
|
|
||
| if (window.parent !== window) { | ||
| window.parent.postMessage({type: 'request_pdf_url'}, '*'); | ||
|
|
||
| function handlePdfUrlResponse(event) { | ||
| if (event.data && event.data.type === 'pdf_url_response') { | ||
| PDF_URL = event.data.url; | ||
|
|
||
| if (PDFViewerApplication.open) { | ||
| PDFViewerApplication.open(PDF_URL); | ||
| PDFViewerApplication.mouseScroll(0); | ||
|
|
||
| setTimeout(function() { | ||
| if (PDFViewerApplication.pdfDocument) { | ||
| if (event.data.title) document.getElementById('titleField').textContent = event.data.title; | ||
| if (event.data.author) document.getElementById('authorField').textContent = event.data.author; | ||
| if (event.data.subject) document.getElementById('subjectField').textContent = event.data.subject; | ||
| if (event.data.keywords) document.getElementById('keywordsField').textContent = event.data.keywords; | ||
| document.getElementById('creatorField').textContent = 'edX Platform'; | ||
| } | ||
| }, 500); | ||
| } | ||
| } else if (event.data && event.data.type === 'chapter_change') { | ||
| window.parent.postMessage({type: 'request_pdf_url'}, '*'); | ||
| } | ||
| } | ||
|
|
||
| window.addEventListener('message', handlePdfUrlResponse); | ||
| } | ||
|
|
||
| document.addEventListener('DOMContentLoaded', function () { | ||
| PDFViewerApplication && PDFViewerApplication.open(PDF_URL); | ||
| }); | ||
| </script> | ||
|
|
||
| <script ${static.url('js/vendor/pdfjs/debugger.js')}></script> | ||
| <script type="text/javascript" src="${static.url('js/vendor/pdfjs/debugger.js')}"></script> | ||
|
|
||
| <%static:js group='main_vendor'/> | ||
| <%static:js group='application'/> | ||
|
|
@@ -347,77 +381,77 @@ | |
|
|
||
| </div> <!-- outerContainer --> | ||
| <div id="printContainer"></div> | ||
| <div id="mozPrintCallback-shim" hidden> | ||
| <style scoped> | ||
| #mozPrintCallback-shim { | ||
| position: fixed; | ||
| top: 0; | ||
| left: 0; | ||
| height: 100%; | ||
| width: 100%; | ||
| z-index: 9999999; | ||
|
|
||
| display: block; | ||
| text-align: center; | ||
| background-color: rgba(0, 0, 0, 0.5); | ||
| } | ||
| #mozPrintCallback-shim[hidden] { | ||
| display: none; | ||
| } | ||
| @media print { | ||
| #mozPrintCallback-shim { | ||
| display: none; | ||
| } | ||
| } | ||
|
|
||
| #mozPrintCallback-shim .mozPrintCallback-dialog-box { | ||
| display: inline-block; | ||
| margin: -50px auto 0; | ||
| position: relative; | ||
| top: 45%; | ||
| left: 0; | ||
| min-width: 220px; | ||
| max-width: 400px; | ||
|
|
||
| padding: 9px; | ||
|
|
||
| border: 1px solid hsla(0, 0%, 0%, .5); | ||
| border-radius: 2px; | ||
| box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); | ||
|
|
||
| background-color: #474747; | ||
|
|
||
| color: hsl(0, 0%, 85%); | ||
| font-size: 16px; | ||
| line-height: 20px; | ||
| } | ||
| #mozPrintCallback-shim .progress-row { | ||
| clear: both; | ||
| padding: 1em 0; | ||
| } | ||
| #mozPrintCallback-shim progress { | ||
| width: 100%; | ||
| } | ||
| #mozPrintCallback-shim .relative-progress { | ||
| clear: both; | ||
| float: right; | ||
| } | ||
| #mozPrintCallback-shim .progress-actions { | ||
| clear: both; | ||
| } | ||
| </style> | ||
| <div class="mozPrintCallback-dialog-box"> | ||
| <!-- TODO: Localise the following strings --> | ||
| Preparing document for printing... | ||
| <div class="progress-row"> | ||
| <progress value="0" max="100"></progress> | ||
| <span class="relative-progress">0%</span> | ||
| </div> | ||
| <div class="progress-actions"> | ||
| <input type="button" value="Cancel" class="mozPrintCallback-cancel"> | ||
| <div id="mozPrintCallback-shim" hidden> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks like it's a formatting cleanup independent of this change, can you make this change in a separate PR to make the delta smaller and easier to review? |
||
| <style scoped> | ||
| #mozPrintCallback-shim { | ||
| position: fixed; | ||
| top: 0; | ||
| left: 0; | ||
| height: 100%; | ||
| width: 100%; | ||
| z-index: 9999999; | ||
|
|
||
| display: block; | ||
| text-align: center; | ||
| background-color: rgba(0, 0, 0, 0.5); | ||
| } | ||
| #mozPrintCallback-shim[hidden] { | ||
| display: none; | ||
| } | ||
| @media print { | ||
| #mozPrintCallback-shim { | ||
| display: none; | ||
| } | ||
| } | ||
|
|
||
| #mozPrintCallback-shim .mozPrintCallback-dialog-box { | ||
| display: inline-block; | ||
| margin: -50px auto 0; | ||
| position: relative; | ||
| top: 45%; | ||
| left: 0; | ||
| min-width: 220px; | ||
| max-width: 400px; | ||
|
|
||
| padding: 9px; | ||
|
|
||
| border: 1px solid hsla(0, 0%, 0%, .5); | ||
| border-radius: 2px; | ||
| box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); | ||
|
|
||
| background-color: #474747; | ||
|
|
||
| color: hsl(0, 0%, 85%); | ||
| font-size: 16px; | ||
| line-height: 20px; | ||
| } | ||
| #mozPrintCallback-shim .progress-row { | ||
| clear: both; | ||
| padding: 1em 0; | ||
| } | ||
| #mozPrintCallback-shim progress { | ||
| width: 100%; | ||
| } | ||
| #mozPrintCallback-shim .relative-progress { | ||
| clear: both; | ||
| float: right; | ||
| } | ||
| #mozPrintCallback-shim .progress-actions { | ||
| clear: both; | ||
| } | ||
| </style> | ||
| <div class="mozPrintCallback-dialog-box"> | ||
| <!-- TODO: Localise the following strings --> | ||
| Preparing document for printing... | ||
| <div class="progress-row"> | ||
| <progress value="0" max="100"></progress> | ||
| <span class="relative-progress">0%</span> | ||
| </div> | ||
| <div class="progress-actions"> | ||
| <input type="button" value="Cancel" class="mozPrintCallback-cancel"> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <script type="text/javascript" src="${static.url('js/vendor/pdfjs/viewer.js')}"></script> | ||
| <script type="text/javascript" src="${static.url('js/pdf-analytics.js')}"></script> | ||
| </body> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a vendored file so we don't want to make changes to it to ease transition to newer versions of the vendored library.