{"record":{"id":"fbb4671fae675614","repo":"davila7/claude-code-templates","slug":"access-denied","errorCode":null,"errorMessage":"Access denied","messagePattern":"Access denied","errorType":"http","errorClass":null,"httpStatus":403,"severity":"warning","filePath":"cli-tool/src/skill-dashboard.js","lineNumber":416,"sourceCode":"        const skillName = req.params.name;\n        const filePath = req.params[0]; // Capture the wildcard path\n\n        await this.loadSkillsData();\n        const skill = this.skills.find(s =>\n          s.name === skillName ||\n          s.name.toLowerCase().replace(/\\s+/g, '-') === skillName.toLowerCase()\n        );\n\n        if (!skill) {\n          return res.status(404).json({ error: 'Skill not found' });\n        }\n\n        const fullPath = path.join(skill.path, filePath);\n\n        // Security check: ensure the file is within the skill directory\n        const normalizedPath = path.normalize(fullPath);\n        if (!normalizedPath.startsWith(skill.path)) {\n          return res.status(403).json({ error: 'Access denied' });\n        }\n\n        if (!(await fs.pathExists(fullPath))) {\n          return res.status(404).json({ error: 'File not found' });\n        }\n\n        const content = await fs.readFile(fullPath, 'utf8');\n        const stats = await fs.stat(fullPath);\n\n        res.json({\n          content,\n          path: filePath,\n          size: this.formatFileSize(stats.size),\n          lastModified: stats.mtime,\n          timestamp: new Date().toISOString()\n        });\n      } catch (error) {\n        console.error('Error loading file:', error);","sourceCodeStart":398,"sourceCodeEnd":434,"githubUrl":"https://github.com/davila7/claude-code-templates/blob/a0851ed10c7c60463dac8cfaaca124cf32d5804d/cli-tool/src/skill-dashboard.js#L398-L434","documentation":"HTTP 403 from the skill dashboard file route when the requested file path, after path.join + path.normalize, does not stay inside skill.path — i.e. a path traversal (../) attempt or a path that resolves outside the skill directory.","triggerScenarios":"GET /api/skills/x/file/../../secrets.json, or absolute paths, or encoded traversal sequences that decode to ../ segments.","commonSituations":"Malicious or buggy frontend requests; manually crafted URLs probing the local server (it binds locally, but the guard still triggers).","solutions":["Request paths strictly relative to the skill root","URL-encode the file path and avoid .. segments","If legitimate file is rejected, verify skill.path is the expected directory"],"exampleFix":"// before\nfetch(`/api/skills/${name}/file/../../config.json`);\n// after\nfetch(`/api/skills/${name}/file/${encodeURIComponent(relPath)}`);","handlingStrategy":"validation","validationCode":"const safeRel = (p) => !p.split('/').includes('..') && !path.isAbsolute(p);","typeGuard":"const isSafeRelPath = (p) => typeof p === 'string' && !p.startsWith('/') && !p.split(/[\\\\/]/).includes('..');","tryCatchPattern":null,"preventionTips":["Never construct file URLs with user-supplied raw paths","URL-encode relative paths and reject '..' segments client-side"],"tags":["http-403","path-traversal","security","skill-dashboard"],"backgroundTag":"path-traversal-blocked","analyzedSha":"a0851ed10c7c60463dac8cfaaca124cf32d5804d","analyzedAt":"2026-08-28T14:11:56.058Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}