{"id":136,"date":"2025-04-30T07:16:06","date_gmt":"2025-04-30T07:16:06","guid":{"rendered":"https:\/\/hackingwithj.com\/?p=136"},"modified":"2025-04-29T07:32:39","modified_gmt":"2025-04-29T07:32:39","slug":"when-secure-apis-leak-through-logging-a-hidden-risk","status":"publish","type":"post","link":"https:\/\/hackingwithj.com\/?p=136","title":{"rendered":"When Secure APIs Leak Through Logging: A Hidden Risk"},"content":{"rendered":"\n<p>In one of the environments I work with, APIs play a key role in delivering functionality \u2014 including the ability to download selected PDF files through a GUI. At first glance, this download feature appears to be well-designed and secure. However, during routine research, I discovered a risk that stems not from the API itself, but from something often overlooked: logging. This post dives into how even secure features can be undermined when logs expose sensitive metadata \u2014 and what I learned from the experience.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">How the Download Process Works<\/h2>\n\n\n\n<p>The GUI lets users download a selection of files bundled into a ZIP archive. Behind the scenes, four endpoints handle this process:<\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><code>POST \/api\/download\/initiate<\/code><\/h4>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Authorization<\/strong>: JWT token (user session-based)<\/li>\n\n\n\n<li><strong>Request<\/strong>: A list of document IDs to be bundled<\/li>\n\n\n\n<li><strong>Response<\/strong>: A job ID for checking download status<\/li>\n<\/ul>\n\n\n\n<h4 class=\"wp-block-heading\"><code>GET \/api\/download\/status\/&lt;job_id&gt;<\/code><\/h4>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Authorization<\/strong>: JWT token<\/li>\n\n\n\n<li><strong>Request<\/strong>: Job ID<\/li>\n\n\n\n<li><strong>Response<\/strong>: Progress or completion status of the ZIP<\/li>\n<\/ul>\n\n\n\n<h4 class=\"wp-block-heading\"><code>GET \/api\/download\/confirm<\/code><\/h4>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Authorization<\/strong>: JWT token<\/li>\n\n\n\n<li><strong>Request<\/strong>: Job ID<\/li>\n\n\n\n<li><strong>Response<\/strong>: An encrypted string used for downloading<\/li>\n<\/ul>\n\n\n\n<h4 class=\"wp-block-heading\"><code>GET \/api\/download\/resource?id=&lt;encrypted_id&gt;<\/code><\/h4>\n\n\n\n<p><strong>Response<\/strong>: The ZIP file containing requested documents<\/p>\n\n\n\n<p><strong>Authorization<\/strong>: None<\/p>\n\n\n\n<p><strong>Request<\/strong>: Encrypted ID<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The Design Strength<\/h3>\n\n\n\n<p>The two strongest points of this implementation:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Strong Encryption<\/strong>: The download ID is encrypted using AES-GCM-256 with a 12-byte IV, tied to the ZIP file\u2019s full path on the server. It\u2019s practically impossible to guess or brute-force this ID.  <\/li>\n\n\n\n<li><strong>JWT-Based Authorization<\/strong>: Every action up until the final download requires a valid, signed token.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">What manipulations did I try?<\/h3>\n\n\n\n<p>This wasn\u2019t an attempt to break things \u2014 I wanted to see how resilient the system was to basic tampering:<\/p>\n\n\n\n<p><strong>Field Manipulation in <code>POST \/initiate<\/code><\/strong><br>I tried:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted has-vivid-purple-color has-text-color has-link-color wp-elements-4bece4d47960c306668ff4b955f1ce47\"><code>{<br>  \"type\": \"ZIP\",<br>  \"ids\": [1, 2, 3]<br>}<br><\/code><\/pre>\n\n\n\n<p>At first, this seemed to work \u2014 until I realized I was testing with an admin-level account. Once I switched to a regular user, the system correctly rejected unauthorized document IDs.<\/p>\n\n\n\n<p><strong>JWT Token Manipulation<\/strong><br>Using <code>jwt_tool<\/code>, I tried tampering with claims to escalate privileges or change access. However, the JWT\u2019s signature is verified server-side and couldn&#8217;t be bypassed.<\/p>\n\n\n\n<p><strong>Brute-forcing the Download Endpoint<\/strong><br>Attempts to directly access the final ZIP download with modified or random encrypted IDs didn\u2019t succeed either \u2014 the encryption key is derived from internal server paths.<\/p>\n\n\n\n<p>Overall, the API design stood firm against:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Broken Object Level Authorization (BOLA)<\/strong><\/li>\n\n\n\n<li><strong>Broken Authentication<\/strong><\/li>\n\n\n\n<li><strong>Broken Function Level Authorization<\/strong><\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">The Hidden Problem: Logging<\/h2>\n\n\n\n<p>Despite all the solid design choices above, the system\u2019s <strong>internal logging<\/strong> introduced an unexpected vulnerability. Millions of logs are generated weekly \u2014 and some of them <strong>include the decrypted download path<\/strong>, which contains metadata like job ID and user identifiers. While this isn\u2019t plain-text document content, it still exposes potentially sensitive information. Worse, access to the logging tool is broadly granted by default when employees join the team \u2014 regardless of role or necessity. That creates unnecessary exposure and potential misuse.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Why This Matters<\/h2>\n\n\n\n<p>You can design your APIs with security best practices \u2014 but if your monitoring, logging, or support systems aren&#8217;t held to the same standard, they can leak valuable information that adversaries could use in the <strong>reconnaissance<\/strong> or <strong>manipulation phase<\/strong> of an attack.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Aftermath &amp; Action Taken<\/h2>\n\n\n\n<p>I brought this to the attention of our internal security officer. We scheduled a session to review the logging configurations and visibility in our monitoring stack. I also submitted a feature request to our development team asking them to <strong>mask sensitive information<\/strong> in logs, especially decrypted file paths and user metadata.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Lessons Learned<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Don\u2019t underestimate the risk of indirect exposure \u2014 <strong>logs and monitoring tools can leak sensitive details<\/strong>.<\/li>\n\n\n\n<li>Apply <strong>role-based access control<\/strong> (RBAC) to internal tools, not just the API.<\/li>\n\n\n\n<li><strong>Review logging output<\/strong> during development and post-deployment.<\/li>\n\n\n\n<li>Consider using <strong>log masking<\/strong> or encrypting fields that reference file paths, user identifiers, or session-specific content.<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>In one of the environments I work with, APIs play a key role in delivering functionality \u2014 including the ability to download selected PDF files through a GUI. At first glance, this download feature appears to be well-designed and secure. However, during routine research, I discovered a risk that stems not from the API itself, [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"site-container-style":"default","site-container-layout":"default","site-sidebar-layout":"default","disable-article-header":"default","disable-site-header":"default","disable-site-footer":"default","disable-content-area-spacing":"default","footnotes":""},"categories":[7],"tags":[16,15],"class_list":["post-136","post","type-post","status-publish","format-standard","hentry","category-realworld","tag-api","tag-logging"],"_links":{"self":[{"href":"https:\/\/hackingwithj.com\/index.php?rest_route=\/wp\/v2\/posts\/136","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/hackingwithj.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/hackingwithj.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/hackingwithj.com\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/hackingwithj.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=136"}],"version-history":[{"count":15,"href":"https:\/\/hackingwithj.com\/index.php?rest_route=\/wp\/v2\/posts\/136\/revisions"}],"predecessor-version":[{"id":153,"href":"https:\/\/hackingwithj.com\/index.php?rest_route=\/wp\/v2\/posts\/136\/revisions\/153"}],"wp:attachment":[{"href":"https:\/\/hackingwithj.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=136"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/hackingwithj.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=136"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/hackingwithj.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=136"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}