(feat) O3-5186: Add security safeguards for logo path configuration#5
(feat) O3-5186: Add security safeguards for logo path configuration#5dkayiwa merged 25 commits intoopenmrs:mainfrom
Conversation
ibacher
left a comment
There was a problem hiding this comment.
Thanks @jnsereko. While most of this is nitpicking, it's as well to keep our documentation tightly focused. Parts of this look LLM-generated, which is perfectly fine, but it's always important to be reviewing the LLM's work and making sure it conforms to our standards, that it doesn't over-document just for the specific task that it's working on (so that our documentation remains clean and readable and primarily aimed at the concerns of people calling the API).
Finally, some actual tests would be very valuable here.
...ain/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java
Outdated
Show resolved
Hide resolved
...ain/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java
Show resolved
Hide resolved
...ain/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java
Outdated
Show resolved
Hide resolved
| * | ||
| * Logo resolution priority: | ||
| * 1. Custom logo from path resolved under {@code OPENMRS_APPLICATION_DATA_DIRECTORY} | ||
| * 2. Default OpenMRS logo as base64 data URI |
There was a problem hiding this comment.
What I mean is we encode the data as a base64 data URL, but that's not something that the caller can supply in logoUrlPath, right? The point of Javadoc comments is to explain to the caller how to use a method.
Here our logo resolution algortihm now looks like:
We try to resolve the
logoUrlPathto a path relative to the Application Data Directory. If this path cannot be resolved to a file, we fallback to using the bytes passed into thedefaultLogoBytesparameter as the logo.
The caller can supply a byte[] representing a default document, but not a base64 data URI.
There was a problem hiding this comment.
I'm also wondering now why we're passing around this byte[] for the default logo? It's passed as an argument here, in the caller (configureHeader()) and it's caller (render()). The issue here is that images are relatively large (a little over 8kB in this case) and arguments to functions are persisted on the Java stack, so we're swallowing ~25kB of memory to pass around data that might be used. It makes more sense, I think, for configureLogo() to load the default logo from the classpath if the logoUrlPath doesn't resolve properly.
...ain/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java
Outdated
Show resolved
Hide resolved
Co-authored-by: Ian <[email protected]>
Co-authored-by: Ian <[email protected]>
Co-authored-by: Ian <[email protected]>
Co-authored-by: Ian <[email protected]>
…/PatientIdStickerXmlReportRenderer.java Co-authored-by: Ian <[email protected]>
|
Ping |
...ain/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java
Outdated
Show resolved
Hide resolved
...ain/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java
Outdated
Show resolved
Hide resolved
| if (logoPath.isAbsolute()) { | ||
| final Path logoRealPath = logoPath.toRealPath(); | ||
| if (!isPathWithinAppDataDirectory(logoRealPath, appDataPath)) { | ||
| throw new APIException("Absolute path must be within application data directory: " + logoUrlPath); |
There was a problem hiding this comment.
How can an absolute path be within the application data directory?
There was a problem hiding this comment.
Absolute paths can be within the application data directory eg
-
Right use
- App Data Directory: /openmrs/data/
- User provides: /openmrs/data/logos/company-logo.png
- This is an absolute path that IS within our app directory ✅
-
Violation
- App Data Directory: /openmrs/data/
- User provides: /etc/passwd
- This is an absolute path that IS NOT within our app directory ❌
There was a problem hiding this comment.
I think that just makes you write more code than you should. Just accept only relative paths within the application data directory. Wouldn't you end up with the same functionality but with less code? 😊
There was a problem hiding this comment.
I think that just makes you write more code than you should. Just accept only relative paths within the application data directory. Wouldn't you end up with the same functionality but with less code? 😊
cc @ibacher
...ain/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java
Outdated
Show resolved
Hide resolved
|
|
||
| return resolvedLogoRealPath.toFile(); | ||
| } catch (IllegalArgumentException e) { | ||
| throw new APIException("Invalid characters in logo path: " + logoUrlPath, e); |
There was a problem hiding this comment.
Is there a possibility of the IllegalArgumentException being thrown while the problem is not that of invalid characters?
There was a problem hiding this comment.
Yes. Thats very true. I have generalized it to Invalid logo path: as I think having multiple checks might not be an overkill.
There was a problem hiding this comment.
What if the log path is valid but the problem is something else?
There was a problem hiding this comment.
Should i change the last catch to catch another error? In this case, null will be returned and the default OpenMRS Logo will be displayed. Something like below.
} catch (Exception e) {
log.error("Failed to resolve logo path: {}", logoUrlPath, e);
return null;
}There was a problem hiding this comment.
If the problem is not with the path itself, then the error message can be misleading. In that case something as generic as i gave above would be better.
There was a problem hiding this comment.
I think something like Failed to access logo file: {} on catching IOException should be better.
...ain/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java
Outdated
Show resolved
Hide resolved
...ain/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java
Outdated
Show resolved
Hide resolved
| } | ||
| else if (isNotBlank(logoUrlPath)) { | ||
| // If a path was provided but we could not resolve or fall back, surface an error | ||
| throw new RenderingException("Failed to configure logo: unresolved path '" + logoUrlPath + "' and no default provided"); |
There was a problem hiding this comment.
Is having a logo a requirement for printing these documents? In other words, is your plan to abort the printing if the only missing part is the logo?
There was a problem hiding this comment.
Logically, each field should be optional so i think we should just log errors instead of throwing them and also basing on the fact that we have a fallback logo.
|
|
||
| return resolvedLogoRealPath.toFile(); | ||
| } catch (IllegalArgumentException e) { | ||
| throw new APIException("Invalid logo path: " + logoUrlPath, e); |
There was a problem hiding this comment.
What criteria do you use to determine whether to throw an exception or log an error?
There was a problem hiding this comment.
Since the logo is an option, like many other sticker configurations, i have just logged the caught errors.
ibacher
left a comment
There was a problem hiding this comment.
There's a bunch of comments here, but the real blocker is that the tests (it's great that we have them!) must pass on CI.
| * | ||
| * Logo resolution priority: | ||
| * 1. Custom logo from path resolved under {@code OPENMRS_APPLICATION_DATA_DIRECTORY} | ||
| * 2. Default OpenMRS logo as base64 data URI |
There was a problem hiding this comment.
What I mean is we encode the data as a base64 data URL, but that's not something that the caller can supply in logoUrlPath, right? The point of Javadoc comments is to explain to the caller how to use a method.
Here our logo resolution algortihm now looks like:
We try to resolve the
logoUrlPathto a path relative to the Application Data Directory. If this path cannot be resolved to a file, we fallback to using the bytes passed into thedefaultLogoBytesparameter as the logo.
The caller can supply a byte[] representing a default document, but not a base64 data URI.
| * | ||
| * Logo resolution priority: | ||
| * 1. Custom logo from path resolved under {@code OPENMRS_APPLICATION_DATA_DIRECTORY} | ||
| * 2. Default OpenMRS logo as base64 data URI |
There was a problem hiding this comment.
I'm also wondering now why we're passing around this byte[] for the default logo? It's passed as an argument here, in the caller (configureHeader()) and it's caller (render()). The issue here is that images are relatively large (a little over 8kB in this case) and arguments to functions are persisted on the Java stack, so we're swallowing ~25kB of memory to pass around data that might be used. It makes more sense, I think, for configureLogo() to load the default logo from the classpath if the logoUrlPath doesn't resolve properly.
...ain/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java
Outdated
Show resolved
Hide resolved
...ain/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java
Outdated
Show resolved
Hide resolved
...ain/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java
Outdated
Show resolved
Hide resolved
| if (isNotBlank(logoUrlPath)) { | ||
| File logoFile = resolveSecureLogoPath(logoUrlPath); | ||
| if (logoFile != null && logoFile.exists() && logoFile.canRead() && logoFile.isFile()) { | ||
| logoContent = logoFile.getAbsolutePath(); |
There was a problem hiding this comment.
Wouldn't it be better to load the image here and convert it into a data URI, as we do for the default logo, rather than passing it as a URL and assuming it will be readable when rendered?
...ain/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java
Outdated
Show resolved
Hide resolved
...ain/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java
Outdated
Show resolved
Hide resolved
…/PatientIdStickerXmlReportRenderer.java Co-authored-by: Ian <[email protected]>
…/PatientIdStickerXmlReportRenderer.java Co-authored-by: Ian <[email protected]>
…/PatientIdStickerXmlReportRenderer.java Co-authored-by: Ian <[email protected]>
…/PatientIdStickerXmlReportRenderer.java Co-authored-by: Ian <[email protected]>
Co-authored-by: Ian <[email protected]>
Co-authored-by: Ian <[email protected]>
…/PatientIdStickerXmlReportRenderer.java Co-authored-by: Ian <[email protected]>
Co-authored-by: Ian <[email protected]>
Co-authored-by: Ian <[email protected]>
|
|
||
| private static final Logger log = LoggerFactory.getLogger(PatientIdStickerXmlReportRenderer.class); | ||
|
|
||
| private static final String DEFAULT_LOGO_CLASSPATH = "web/module/resources/openmrs_logo_white_large.png"; |
There was a problem hiding this comment.
Are you still loading the logo from the module?
There was a problem hiding this comment.
I thought that we had agreed not to duplicate this logo in the module.
There was a problem hiding this comment.
Yes. We definitely had agreed that but i am out of options.
@ibacher I'm also wondering now why we're passing around this byte[] for the default logo? It's passed as an argument here, in the caller (configureHeader()) and it's caller (render()). The issue here is that images are relatively large (a little over 8kB in this case) and arguments to functions are persisted on the Java stack, so we're swallowing ~25kB of memory to pass around data that might be used. It makes more sense, I think, for configureLogo() to load the default logo from the classpath if the logoUrlPath doesn't resolve properly.
We had two options
-
Get OpenMRS logo from servlet context (only be one in controllers)
- pass OpenMRS logo data from controllers to images (not recommended~ increasing memory in method calls) ❌
- save as a temporary or permanent file (not recommended ~ duplicate file on disc) ❌
-
Using classpath
- get OpenMRS logo from legacy UI resources
OpenmrsClassLoader.getInstance().getResourceAsStream()(not working) ❌ - duplicate OpenMRS logo in this module and access it using classpath (only option left) ✅
- get OpenMRS logo from legacy UI resources
...ain/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java
Outdated
Show resolved
Hide resolved
| final Path appDataPath = appDataDir.toPath().toRealPath(); | ||
| final Path logoPath = Paths.get(logoUrlPath); | ||
|
|
||
| // For absolute paths, verify they're within app data directory |
There was a problem hiding this comment.
Didn't we agree to remove this?
There was a problem hiding this comment.
@ibacher suggested that we can allow absolute paths so long as they are with in the application data directory.
something like /openmrs/data/custom_logo.png
There was a problem hiding this comment.
I'm happy to leave this off, but we've had requests for supporting this for similar features where we've tried to lock-down where files are loaded from.
…be null)) Removed null check for application data directory.
|
@jnsereko is there a reason why you are still accepting absolute paths for the logo? I thought we agreed to only allow relative paths. |
|
Thank you @dkayiwa |
| final Path resolvedLogoRealPath = resolvedLogoPath.toRealPath(); | ||
|
|
||
| if (!isPathWithinAppDataDirectory(resolvedLogoRealPath, appDataPath)) { | ||
| log.error("Logo path escapes application data directory: {}", logoUrlPath); |
There was a problem hiding this comment.
I have changed the log to Logo path must be within the application data directory
Description
This PR implements security protections for the logo path configuration feature to prevent path traversal attacks.
The changes restrict logo file access to only the OpenMRS application data directory, addressing security concerns that were blocking O3 inclusion.
Screenshot
Related issue
https://openmrs.atlassian.net/browse/O3-5186