coretrace-python-analyzer 0.12.0__tar.gz → 0.14.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (278) hide show
  1. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/CHANGELOG.md +20 -0
  2. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/PKG-INFO +1 -1
  3. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/docs/plugins.md +7 -0
  4. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/docs/usage.md +5 -5
  5. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/pyproject.toml +1 -1
  6. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/__init__.py +1 -1
  7. coretrace_python_analyzer-0.14.0/src/coretrace_python/abstract/strings.py +98 -0
  8. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/models/aiohttp/aiohttp_models.py +22 -10
  9. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/models/django/django_models.py +19 -0
  10. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/models/tornado/tornado_models.py +16 -1
  11. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/security/ssrf/plugin.toml +1 -1
  12. coretrace_python_analyzer-0.14.0/src/coretrace_python/bundled/security/ssrf/ssrf.py +64 -0
  13. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/engine.py +2 -0
  14. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/plugins/detectors.py +13 -2
  15. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/taint/__init__.py +2 -0
  16. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/taint/engine.py +39 -12
  17. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/taint/models.py +25 -0
  18. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_detectors.py +4 -1
  19. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_nosql_injection.py +136 -10
  20. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_release.py +1 -1
  21. coretrace_python_analyzer-0.14.0/tests/test_ssrf_destinations.py +154 -0
  22. coretrace_python_analyzer-0.12.0/src/coretrace_python/bundled/security/ssrf/ssrf.py +0 -17
  23. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/.github/workflows/ci.yml +0 -0
  24. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/.github/workflows/release.yml +0 -0
  25. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/.gitignore +0 -0
  26. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/LICENSE +0 -0
  27. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/NOTICE +0 -0
  28. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/README.md +0 -0
  29. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/docs/architecture.md +0 -0
  30. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/examples/add.py +0 -0
  31. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/examples/canonical_symbols.py +0 -0
  32. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/__main__.py +0 -0
  33. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/abstract/__init__.py +0 -0
  34. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/abstract/constants.py +0 -0
  35. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/abstract/heap.py +0 -0
  36. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/abstract/ranges.py +0 -0
  37. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/abstract/values.py +0 -0
  38. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/analysis/__init__.py +0 -0
  39. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/analysis/manager.py +0 -0
  40. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/analysis/provider.py +0 -0
  41. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/dependency/dependency_policy/dependency_policy.py +0 -0
  42. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/dependency/dependency_policy/plugin.toml +0 -0
  43. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/dependency/reachable_vulnerability/plugin.toml +0 -0
  44. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/dependency/reachable_vulnerability/reachable_vulnerability.py +0 -0
  45. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/dependency/sample_advisories/plugin.toml +0 -0
  46. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/dependency/sample_advisories/sample_advisories.py +0 -0
  47. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/dependency/vulnerable_dependency/plugin.toml +0 -0
  48. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/dependency/vulnerable_dependency/vulnerable_dependency.py +0 -0
  49. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/models/aiohttp/plugin.toml +0 -0
  50. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/models/bottle/bottle_models.py +0 -0
  51. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/models/bottle/plugin.toml +0 -0
  52. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/models/cli/cli_models.py +0 -0
  53. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/models/cli/plugin.toml +0 -0
  54. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/models/credentials/credential_models.py +0 -0
  55. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/models/credentials/plugin.toml +0 -0
  56. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/models/db_drivers/db_driver_models.py +0 -0
  57. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/models/db_drivers/plugin.toml +0 -0
  58. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/models/django/plugin.toml +0 -0
  59. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/models/fastapi/fastapi_models.py +0 -0
  60. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/models/fastapi/plugin.toml +0 -0
  61. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/models/flask/flask_models.py +0 -0
  62. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/models/flask/plugin.toml +0 -0
  63. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/models/http_clients/http_client_models.py +0 -0
  64. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/models/http_clients/plugin.toml +0 -0
  65. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/models/python_stdlib/plugin.toml +0 -0
  66. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/models/python_stdlib/python_stdlib.py +0 -0
  67. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/models/sqlalchemy/plugin.toml +0 -0
  68. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/models/sqlalchemy/sqlalchemy_models.py +0 -0
  69. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/models/tornado/plugin.toml +0 -0
  70. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/secrets/config_secrets/config_secrets.py +0 -0
  71. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/secrets/config_secrets/plugin.toml +0 -0
  72. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/secrets/hardcoded_secrets/hardcoded_secrets.py +0 -0
  73. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/secrets/hardcoded_secrets/plugin.toml +0 -0
  74. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/security/code_injection/code_injection.py +0 -0
  75. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/security/code_injection/plugin.toml +0 -0
  76. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/security/command_injection/command_injection.py +0 -0
  77. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/security/command_injection/plugin.toml +0 -0
  78. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/security/insecure_deserialization/insecure_deserialization.py +0 -0
  79. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/security/insecure_deserialization/plugin.toml +0 -0
  80. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/security/nosql_injection/nosql_injection.py +0 -0
  81. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/security/nosql_injection/plugin.toml +0 -0
  82. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/security/open_redirect/open_redirect.py +0 -0
  83. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/security/open_redirect/plugin.toml +0 -0
  84. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/security/path_traversal/path_traversal.py +0 -0
  85. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/security/path_traversal/plugin.toml +0 -0
  86. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/security/plaintext_credentials/plaintext_credentials.py +0 -0
  87. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/security/plaintext_credentials/plugin.toml +0 -0
  88. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/security/sql_injection/plugin.toml +0 -0
  89. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/security/sql_injection/sql_injection.py +0 -0
  90. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/security/xss/plugin.toml +0 -0
  91. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/security/xss/xss.py +0 -0
  92. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/syntax/dangerous_eval/dangerous_eval.py +0 -0
  93. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/syntax/dangerous_eval/plugin.toml +0 -0
  94. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/syntax/flask_debug/flask_debug.py +0 -0
  95. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/syntax/flask_debug/plugin.toml +0 -0
  96. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/syntax/insecure_tempfile/insecure_tempfile.py +0 -0
  97. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/syntax/insecure_tempfile/plugin.toml +0 -0
  98. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/syntax/insecure_tls/insecure_tls.py +0 -0
  99. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/syntax/insecure_tls/plugin.toml +0 -0
  100. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/syntax/missing_timeout/missing_timeout.py +0 -0
  101. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/syntax/missing_timeout/plugin.toml +0 -0
  102. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/syntax/unsafe_archive/plugin.toml +0 -0
  103. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/syntax/unsafe_archive/unsafe_archive.py +0 -0
  104. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/syntax/unsafe_xml/plugin.toml +0 -0
  105. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/syntax/unsafe_xml/unsafe_xml.py +0 -0
  106. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/syntax/weak_crypto/plugin.toml +0 -0
  107. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/syntax/weak_crypto/weak_crypto.py +0 -0
  108. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/syntax/weak_random/plugin.toml +0 -0
  109. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/bundled/syntax/weak_random/weak_random.py +0 -0
  110. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/cache.py +0 -0
  111. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/cfg/__init__.py +0 -0
  112. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/cfg/builder.py +0 -0
  113. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/cfg/dominance.py +0 -0
  114. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/cfg/model.py +0 -0
  115. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/cli.py +0 -0
  116. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/dataflow/__init__.py +0 -0
  117. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/dataflow/lattice.py +0 -0
  118. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/dataflow/solver.py +0 -0
  119. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/dependency/__init__.py +0 -0
  120. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/dependency/advisories.py +0 -0
  121. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/dependency/correlation.py +0 -0
  122. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/dependency/graph.py +0 -0
  123. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/dependency/policy.py +0 -0
  124. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/dependency/sbom.py +0 -0
  125. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/dependency/vex.py +0 -0
  126. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/findings/__init__.py +0 -0
  127. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/findings/baseline.py +0 -0
  128. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/findings/coverage.py +0 -0
  129. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/findings/model.py +0 -0
  130. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/findings/provenance.py +0 -0
  131. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/findings/refutation.py +0 -0
  132. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/findings/suppressions.py +0 -0
  133. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/frontend/__init__.py +0 -0
  134. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/frontend/ast_adapter.py +0 -0
  135. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/frontend/parser.py +0 -0
  136. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/hir/__init__.py +0 -0
  137. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/hir/nodes.py +0 -0
  138. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/hir/visitors.py +0 -0
  139. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/interprocedural/__init__.py +0 -0
  140. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/interprocedural/callgraph.py +0 -0
  141. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/interprocedural/modulegraph.py +0 -0
  142. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/interprocedural/summaries.py +0 -0
  143. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/ir/__init__.py +0 -0
  144. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/ir/defuse.py +0 -0
  145. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/ir/lowering.py +0 -0
  146. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/ir/model.py +0 -0
  147. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/ir/printer.py +0 -0
  148. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/ir/ssa.py +0 -0
  149. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/plugins/__init__.py +0 -0
  150. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/plugins/api.py +0 -0
  151. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/plugins/loader.py +0 -0
  152. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/plugins/manifest.py +0 -0
  153. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/plugins/registry.py +0 -0
  154. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/plugins/secrets.py +0 -0
  155. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/reporters/__init__.py +0 -0
  156. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/reporters/json_format.py +0 -0
  157. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/reporters/report.py +0 -0
  158. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/reporters/sarif.py +0 -0
  159. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/reporters/text.py +0 -0
  160. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/semantic/__init__.py +0 -0
  161. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/semantic/identity.py +0 -0
  162. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/semantic/imports.py +0 -0
  163. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/semantic/scopes.py +0 -0
  164. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/semantic/symbols.py +0 -0
  165. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/source/__init__.py +0 -0
  166. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/source/manager.py +0 -0
  167. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/source/model.py +0 -0
  168. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/taint/routes.py +0 -0
  169. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/src/coretrace_python/taint/templates.py +0 -0
  170. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/regression/expected/Chatbot.json +0 -0
  171. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/regression/expected/DjangoGoat.json +0 -0
  172. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/regression/expected/Flask-Image-Editing-Website.json +0 -0
  173. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/regression/expected/InsecureGram.json +0 -0
  174. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/regression/expected/MiniBookApiServer.json +0 -0
  175. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/regression/expected/NPL-IA.json +0 -0
  176. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/regression/expected/Spam-Detection-Project.json +0 -0
  177. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/regression/expected/Vulnerable_fastapi.json +0 -0
  178. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/regression/expected/dvpwa.json +0 -0
  179. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/regression/expected/full-stack-fastapi-template.json +0 -0
  180. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/regression/expected/healthchecks.json +0 -0
  181. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/regression/expected/httpie.json +0 -0
  182. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/regression/expected/luigi.json +0 -0
  183. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/regression/expected/my_gdb.json +0 -0
  184. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/regression/expected/network-topologer.json +0 -0
  185. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/regression/expected/pygoat.json +0 -0
  186. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/regression/expected/uploadbox-cloud-storage-solution.json +0 -0
  187. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/regression/expected/vulnerable-flask.json +0 -0
  188. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/regression/expected/yt-fts.json +0 -0
  189. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/regression/fixtures/vulnerable-flask/README.md +0 -0
  190. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/regression/fixtures/vulnerable-flask/app.py +0 -0
  191. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/regression/fixtures/vulnerable-flask/requirements.txt +0 -0
  192. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/regression/fixtures/vulnerable-flask/vulnerable_services.py +0 -0
  193. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/regression/repositories.toml +0 -0
  194. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_advisory_evidence.py +0 -0
  195. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_advisory_feeds.py +0 -0
  196. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_aiohttp_models.py +0 -0
  197. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_analysis_manager.py +0 -0
  198. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_architecture.py +0 -0
  199. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_argument_conditions.py +0 -0
  200. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_attacker_arguments.py +0 -0
  201. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_audit_detectors.py +0 -0
  202. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_baseline.py +0 -0
  203. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_cache.py +0 -0
  204. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_cfg.py +0 -0
  205. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_class_keywords_with_targets.py +0 -0
  206. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_cli.py +0 -0
  207. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_closures.py +0 -0
  208. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_code_injection.py +0 -0
  209. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_config_secrets_coverage.py +0 -0
  210. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_constants.py +0 -0
  211. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_correlation.py +0 -0
  212. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_credential_context.py +0 -0
  213. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_dataflow.py +0 -0
  214. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_dependencies.py +0 -0
  215. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_discovery.py +0 -0
  216. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_django_http_clients.py +0 -0
  217. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_django_routes.py +0 -0
  218. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_django_safe_values.py +0 -0
  219. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_docs.py +0 -0
  220. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_dominance.py +0 -0
  221. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_emit_ir.py +0 -0
  222. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_exceptions.py +0 -0
  223. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_factories_match_loop_else.py +0 -0
  224. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_fail_on.py +0 -0
  225. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_fastapi_cli_precision.py +0 -0
  226. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_frameworks.py +0 -0
  227. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_frontend_gaps.py +0 -0
  228. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_heap.py +0 -0
  229. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_hex_digests.py +0 -0
  230. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_hir_builder.py +0 -0
  231. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_imports.py +0 -0
  232. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_interprocedural.py +0 -0
  233. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_interprocedural_taint.py +0 -0
  234. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_ir.py +0 -0
  235. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_match_patterns.py +0 -0
  236. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_members.py +0 -0
  237. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_module_level.py +0 -0
  238. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_nonlocal_writeback.py +0 -0
  239. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_operator_sources.py +0 -0
  240. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_packaging.py +0 -0
  241. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_parallel.py +0 -0
  242. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_parser.py +0 -0
  243. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_plaintext_credentials.py +0 -0
  244. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_plugins.py +0 -0
  245. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_project.py +0 -0
  246. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_project_models.py +0 -0
  247. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_project_validators.py +0 -0
  248. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_provenance.py +0 -0
  249. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_pyir_imports.py +0 -0
  250. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_read_entry_points.py +0 -0
  251. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_real_code_precision.py +0 -0
  252. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_redirect_targets.py +0 -0
  253. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_refutation.py +0 -0
  254. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_refutation_evidence.py +0 -0
  255. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_regression.py +0 -0
  256. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_relative_paths.py +0 -0
  257. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_remaining_limits.py +0 -0
  258. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_reporters.py +0 -0
  259. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_safe_arguments.py +0 -0
  260. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_scale.py +0 -0
  261. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_scopes.py +0 -0
  262. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_secrets_sbom.py +0 -0
  263. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_self_state.py +0 -0
  264. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_source_manager.py +0 -0
  265. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_ssa.py +0 -0
  266. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_ssrf_targets.py +0 -0
  267. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_summary_sanitizers.py +0 -0
  268. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_suppressions.py +0 -0
  269. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_symbol_resolution.py +0 -0
  270. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_symbols.py +0 -0
  271. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_syntactic_rules.py +0 -0
  272. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_syntax_control_flow.py +0 -0
  273. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_syntax_coverage.py +0 -0
  274. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_syntax_expressions.py +0 -0
  275. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_taint.py +0 -0
  276. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_template_escaping.py +0 -0
  277. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_tornado_bottle_models.py +0 -0
  278. {coretrace_python_analyzer-0.12.0 → coretrace_python_analyzer-0.14.0}/tests/test_vex.py +0 -0
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.14.0 (2026-09-26)
4
+
5
+ ### Precision
6
+
7
+ - `ssrf` judges a flow on how `+` and f-strings build its URL. A fixed host is a limited proof: when constant text fixes `scheme://host` and ends the authority with `/`, `?` or `#` before any input, as in `"https://api.example.com/users/" + id`, the flow is a hotspot, because the input still chooses the path and a redirect may lead elsewhere. It is refuted only when the constant text fixes the path too, the input reaching the query or fragment only, and the call disables redirects (`allow_redirects=False`, `follow_redirects=False`). A module-level name counts as known text when the module binds it once to a string literal and never rebinds it. Everything else stays a vulnerability, including a base of unknown value such as an imported `BASE_URL`, which may leave the authority open. The advisory correlation keeps its own verdict. The regression corpus is unchanged: its six `ssrf` findings build their URL from values of unknown content (#165).
8
+
9
+ ### Plugins
10
+
11
+ - `TaintDetector.judge(ctx, function, flow, verdict)` lets a rule refine the refutation's verdict on a flow for its own findings, without changing the verdict other consumers of the flow read (#165).
12
+
13
+ ## 0.13.0 (2026-09-26)
14
+
15
+ ### Precision
16
+
17
+ - The text attributes and URL parameters of a request object carry no `NOSQL`. Django, REST framework, aiohttp and tornado views read a request tainted as a whole, so `request.GET["name"]` and a URL parameter were `nosql-injection` like the body. Now `GET`, `POST`, `COOKIES`, `META`, `headers`, `query_params`, aiohttp's `query` and `match_info`, tornado's `arguments` and the other text attributes are text, and so are the URL parameters a view receives after the request. The body, the files, REST framework's `data` and aiohttp's `json()` keep the kinds of the request. The other kinds are unchanged, and so is the regression corpus. A helper function the request is passed to still reads it as a whole (#162).
18
+
19
+ ### Plugins
20
+
21
+ - A `RequestObject(symbol, text=())` model says that the inputs from an entry point, a route registrar, a typed parameter or a source are request objects, and which of their attributes are text (#162).
22
+
3
23
  ## 0.12.0 (2026-09-26)
4
24
 
5
25
  ### Detection
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: coretrace-python-analyzer
3
- Version: 0.12.0
3
+ Version: 0.14.0
4
4
  Summary: Static security analysis infrastructure for Python code
5
5
  Project-URL: Homepage, https://github.com/CoreTrace/coretrace-python-analyzer
6
6
  Project-URL: Repository, https://github.com/CoreTrace/coretrace-python-analyzer
@@ -115,6 +115,12 @@ ordinary input reaching a database write is not a plaintext credential; `LOG`, f
115
115
  values that must not be written to a log as they are; and `PII`, for personal data
116
116
  that must not leave the application through a log, a third party or plain storage.
117
117
 
118
+ A rule may know more about its own sinks than the refutation does: `judge(ctx, function,
119
+ flow, verdict)` returns the rule's verdict on a flow, given the refutation's, and by
120
+ default returns it unchanged. `ssrf` reads there how the URL is built. The refutation's
121
+ verdict, which other consumers of the flow such as the advisory correlation read, does
122
+ not change; a rule reading more analyses declares them in its `requires`.
123
+
118
124
  ### Security models: `ModelPlugin`
119
125
 
120
126
  Declares sources, sinks and sanitizers for a library or a framework. A model plugin
@@ -151,6 +157,7 @@ class BottleModels(ModelPlugin):
151
157
  | `SafeArgument(symbol, argument, values, position=None, kinds=ALL)` | A call to the sink `symbol` whose `argument` (by keyword, or at `position`) denotes one of `values` is not a sink for `kinds`, such as `yaml.load` with `Loader=yaml.SafeLoader`. Values are written as the analyzer records arguments: a symbol by its canonical name, a constant as Python writes it (`True`). Only a value given explicitly makes the call safe. |
152
158
  | `TemplateRender(symbol, position=0, keyword="template_name")` | A call rendering, with autoescaping, the template named by its argument at `position` or `keyword`, such as `render_to_string`. What it returns carries no `HTML` when the name is written in the call and the project's template escapes everything it prints (see the usage guide). |
153
159
  | `Validator(symbol, kinds=ALL, argument=0)` | A callable whose truth proves its argument safe for `kinds`, such as `re.fullmatch`. A flow guarded by it is refuted when it carries no other kind into the sink, and is a hotspot otherwise: a redirect validator does not make a shell command safe. `symbol` may be a function of the project, by its project symbol (`python.hc.accounts.views._allow_redirect`), which then counts wherever it is called, in its own module too. |
160
+ | `RequestObject(symbol, text=())` | The inputs from `symbol`, an entry point, a route registrar, a typed parameter or a source, are request objects. A view receives the request first (after `self`), then URL parameters, which are text. Reading one of the request's `text` attributes gives text, as Django's `GET` or aiohttp's `query`; its body and files keep the request's kinds. An attribute read counts only when every source of the value is such a request. |
154
161
  | `AuthorizationGuard(symbol, label)` | A decorator or a condition restricting who reaches the code, such as `login_required`; a flow behind it is a hotspot. |
155
162
  | `Members(symbol, dynamic=None, defined=(), typed=())` | What the attributes and items of an instance of the class `symbol` give, for objects that give others by name. `typed` maps an attribute to the class of what it gives, called or not, since a call denotes its callee: pymongo's `get_database` gives a `Database`, as Flask-PyMongo's `db` property does. `dynamic`, when set, is the class of every item and of every attribute outside `defined` and `typed`: a pymongo client gives a `Database` by name (`client.shop`, `client["shop"]`), a database a `Collection`. Every path to a collection then denotes the collection class, so a sink on `Collection.find` covers them all. Without `dynamic`, an attribute keeps its path and an item its container's symbol, as for any class. |
156
163
 
@@ -112,9 +112,9 @@ variable is not treated that way yet.
112
112
  | `command-injection` | Shell or process execution (`os.system`, `subprocess` with a string, …). |
113
113
  | `code-injection` | The code `eval` or `exec` runs; the globals and locals passed with it are data. `dangerous-eval` reports every such call, this rule the ones attacker input reaches. |
114
114
  | `sql-injection` | A database statement (`cursor.execute`, SQLAlchemy `text`, Django `raw`, …). Parameters of a parameterised query are not statements. |
115
- | `nosql-injection` | A NoSQL query, such as a MongoDB filter, given attacker input that may hold a structure: a JSON body giving `{"$ne": null}` matches any value. The input that may hold a structure is JSON, the raw bodies a `json.loads` decodes, files, WebSocket messages and standard input. Text cannot hold an operator: form fields, query parameters, headers, cookies, URL parameters, the command line, the environment, and an entry-point parameter annotated with scalars (`str`, `int`, `Optional[str]`, `Annotated[str, Query()]`, `list[str]`, …), which FastAPI validates. Limits: a Django or aiohttp request object is tainted as a whole, so `request.GET` counts like `request.body`; the keys of a dict are not followed, so a string concatenated into `$where` is not reported. The analyzer ships no NoSQL sink: model plugins declare them. |
115
+ | `nosql-injection` | A NoSQL query, such as a MongoDB filter, given attacker input that may hold a structure: a JSON body giving `{"$ne": null}` matches any value. The input that may hold a structure is JSON, the raw bodies a `json.loads` decodes, files, WebSocket messages and standard input. Text cannot hold an operator: form fields, query parameters, headers, cookies, URL parameters, the command line, the environment, and an entry-point parameter annotated with scalars (`str`, `int`, `Optional[str]`, `Annotated[str, Query()]`, `list[str]`, …), which FastAPI validates. A Django, REST framework, aiohttp or tornado view reads a request object: its text attributes (`GET`, `POST`, `headers`, `query_params`, `query`, `match_info`, `arguments`, …) and the URL parameters after it are text, its body, files and `data` are not. Limits: a helper function the request is passed to reads it as a whole; the keys of a dict are not followed, so a string concatenated into `$where` is not reported. The analyzer ships no NoSQL sink: model plugins declare them. |
116
116
  | `path-traversal` | A file system path (`open`, `send_file`, `os.remove`, …). |
117
- | `ssrf` | The URL of an HTTP client request (Requests, httpx, `urllib`), by position or as `url=`; the body, headers and query parameters are not the destination. |
117
+ | `ssrf` | The URL of an HTTP client request (Requests, httpx, `urllib`), by position or as `url=`; the body, headers and query parameters are not the destination. How `+` and f-strings build the URL can prove part of its destination. When constant text fixes `scheme://host` and ends the authority with `/`, `?` or `#` before any input (`"https://api.example.com/users/" + id`, or a module-level string bound once and never rebound), the flow is a hotspot: the input still chooses the path, which may reach a sensitive resource, and a redirect may lead elsewhere. With the path fixed too, the input reaching the query or fragment only, and the call disabling redirects (`allow_redirects=False`, `follow_redirects=False`), the destination is fixed and the flow is refuted. Anything unproven stays a vulnerability: a base of unknown value (imported, a call result), a constant leaving the authority open (`"https:/"`, no `/` after the host, a variable port), `urljoin`, `%`, `format`, a URL built in a helper function. |
118
118
  | `xss` | An HTTP response body built without escaping. A Django template rendered by `render_to_string` escapes, where the analyzer can establish it (below). |
119
119
  | `insecure-deserialization` | `pickle`, `marshal`, `dill`, `jsonpickle`, `shelve`, and the YAML loaders (`yaml.load`, `load_all`, `full_load`, `unsafe_load` and their several-document versions); `yaml.load` and `load_all` given a safe loader (`SafeLoader`, `BaseLoader`) are not. |
120
120
  | `open-redirect` | An HTTP redirect target. |
@@ -241,7 +241,7 @@ the directory of the checked file; a path outside it is printed as it is.
241
241
  ```json
242
242
  {
243
243
  "schema_version": 1,
244
- "tool": {"name": "coretrace-python-analyzer", "version": "0.12.0"},
244
+ "tool": {"name": "coretrace-python-analyzer", "version": "0.14.0"},
245
245
  "root": "/home/me/project",
246
246
  "findings": [
247
247
  {
@@ -273,7 +273,7 @@ bytes. A single-file check lists no components.
273
273
 
274
274
  ```json
275
275
  "tool": {
276
- "name": "coretrace-python-analyzer", "version": "0.12.0",
276
+ "name": "coretrace-python-analyzer", "version": "0.14.0",
277
277
  "components": [
278
278
  {"kind": "plugin", "name": "curated-advisories", "version": "2026.09.25", "digest": "sha256:…"},
279
279
  {"kind": "advisories", "name": "advisories.json", "digest": "sha256:…"}
@@ -436,7 +436,7 @@ project's behalf would go unseen. Without a lock file, a package is never
436
436
  `not_affected`. The document's `@id` derives from its statements, so two runs with the
437
437
  same result share it; its `author` is `Unknown Author`, as OpenVEX tools write when they
438
438
  cannot know it. Its `tooling` names the analyzer and each component with its digest
439
- (`coretrace-python-analyzer 0.12.0; curated-advisories 2026.09.25 (sha256:…)`), so the
439
+ (`coretrace-python-analyzer 0.14.0; curated-advisories 2026.09.25 (sha256:…)`), so the
440
440
  `@id` changes with the advisory data a statement was decided with.
441
441
 
442
442
  ## Large projects
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "coretrace-python-analyzer"
7
- version = "0.12.0"
7
+ version = "0.14.0"
8
8
  description = "Static security analysis infrastructure for Python code"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,4 +1,4 @@
1
1
  """CoreTrace's Python static analysis frontend."""
2
2
 
3
- __version__ = "0.12.0"
3
+ __version__ = "0.14.0"
4
4
 
@@ -0,0 +1,98 @@
1
+ """The constant text a string starts with (architecture §18): what ``+`` and f-strings
2
+ build from string literals and from module-level names bound once to one. Anything
3
+ else, a parameter, a call result, an import, ``%`` or ``format``, is text of unknown
4
+ value."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from collections.abc import Mapping
9
+ from types import MappingProxyType
10
+ from typing import ClassVar
11
+
12
+ from coretrace_python.analysis import Analysis, AnalysisContext, AnyAnalysis
13
+ from coretrace_python.cfg import CFGError
14
+ from coretrace_python.hir import nodes
15
+ from coretrace_python.ir.lowering import (
16
+ MODULE_BODY,
17
+ LoweringError,
18
+ PyIRAnalysis,
19
+ analyzable_functions,
20
+ )
21
+ from coretrace_python.ir.model import (
22
+ BinaryOp,
23
+ BuildString,
24
+ Constant,
25
+ ForNext,
26
+ Global,
27
+ Instruction,
28
+ SetGlobal,
29
+ StoreLocal,
30
+ Value,
31
+ )
32
+
33
+
34
+ def leading_text(value: Value, defs: Mapping[Value, Instruction], strings: Mapping[str, str]) -> tuple[str, bool]:
35
+ """The constant text ``value`` starts with, and whether that text is all of it, given
36
+ the definitions of the function's values and the module's ``strings``."""
37
+
38
+ made = defs.get(value)
39
+ if isinstance(made, Constant):
40
+ return (made.value, True) if isinstance(made.value, str) else ("", False)
41
+ if isinstance(made, Global):
42
+ known = strings.get(made.name)
43
+ return ("", False) if known is None else (known, True)
44
+ if isinstance(made, BinaryOp) and made.operator == "add":
45
+ left, whole = leading_text(made.left, defs, strings)
46
+ if not whole:
47
+ return left, False
48
+ right, whole = leading_text(made.right, defs, strings)
49
+ return left + right, whole
50
+ if isinstance(made, BuildString):
51
+ text = ""
52
+ for part in made.parts:
53
+ piece, whole = leading_text(part, defs, strings)
54
+ text += piece
55
+ if not whole:
56
+ return text, False
57
+ return text, True
58
+ return "", False
59
+
60
+
61
+ class ModuleStringsAnalysis(Analysis[Mapping[str, str]]):
62
+ """The module-level names bound once, to a string literal, and never rebound: by no
63
+ other statement of the module body, no function assigning them as ``global``, no
64
+ definition of that name. A module that does not lower entirely has none."""
65
+
66
+ name: ClassVar[str] = "abstract.module_strings"
67
+ requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({PyIRAnalysis})
68
+
69
+ @classmethod
70
+ def compute(cls, ctx: AnalysisContext) -> Mapping[str, str]:
71
+ try:
72
+ functions = [ctx.get(PyIRAnalysis, function) for function in analyzable_functions(ctx.module)]
73
+ except (LoweringError, CFGError):
74
+ return MappingProxyType({})
75
+ body = next((function for function in functions if function.name == MODULE_BODY), None)
76
+ if body is None:
77
+ return MappingProxyType({})
78
+ defs = {i.result: i for block in body.blocks for i in block.instructions if i.result is not None}
79
+ stored: dict[str, list[Value | None]] = {}
80
+ for block in body.blocks:
81
+ for instruction in block.instructions:
82
+ if isinstance(instruction, StoreLocal):
83
+ stored.setdefault(instruction.name, []).append(instruction.value)
84
+ if isinstance(block.terminator, ForNext):
85
+ # A ``for`` binds its target to each item.
86
+ stored.setdefault(block.terminator.target, []).append(None)
87
+ rebound = {
88
+ i.name for function in functions for block in function.blocks for i in block.instructions
89
+ if isinstance(i, SetGlobal)
90
+ }
91
+ rebound |= {s.name for s in ctx.module.body if isinstance(s, nodes.Function | nodes.Class)}
92
+ found: dict[str, str] = {}
93
+ for name, values in stored.items():
94
+ value = values[0] if len(values) == 1 and name not in rebound else None
95
+ made = defs.get(value) if value is not None else None
96
+ if isinstance(made, Constant) and isinstance(made.value, str):
97
+ found[name] = made.value
98
+ return MappingProxyType(found)
@@ -12,6 +12,7 @@ from coretrace_python.semantic.symbols import SymbolId
12
12
  from coretrace_python.taint import (
13
13
  EntryPoint,
14
14
  Model,
15
+ RequestObject,
15
16
  RouteRegistrar,
16
17
  Sink,
17
18
  Source,
@@ -22,6 +23,23 @@ from coretrace_python.taint import (
22
23
  _METHODS = ("get", "post", "put", "patch", "delete", "head", "options")
23
24
  _REQUEST_CLASSES = ("aiohttp.web.Request", "aiohttp.web_request.Request")
24
25
  _ROUTERS = ("aiohttp.web.Application.router", "aiohttp.web.UrlDispatcher")
26
+ # ``@routes.get('/')`` on a ``RouteTableDef``.
27
+ _ROUTE_TABLES = tuple(f"aiohttp.web.RouteTableDef.{method}" for method in (*_METHODS, "route", "view"))
28
+ # ``app.router.add_route('GET', '/', handler)``, ``app.router.add_get('/', handler)`` and
29
+ # ``app.add_routes([web.get('/', handler)])``, each with the argument naming the handler.
30
+ _ROUTE_CALLS = (
31
+ *((f"{router}.add_route", 2) for router in _ROUTERS),
32
+ *((f"{router}.add_{method}", 1) for router in _ROUTERS for method in (*_METHODS, "view")),
33
+ ("aiohttp.web.route", 2),
34
+ *((f"aiohttp.web.{method}", 1) for method in (*_METHODS, "view")),
35
+ )
36
+ # What a request gives as text; its body (``json()``, ``text()``, ``read()``) may hold a
37
+ # structure.
38
+ _REQUEST_TEXT = (
39
+ "query", "rel_url", "url", "path", "raw_path", "path_qs", "query_string", "match_info", "headers",
40
+ "raw_headers", "cookies", "method", "host", "scheme", "remote", "forwarded", "content_type", "charset",
41
+ "if_modified_since", "post",
42
+ )
25
43
  _REDIRECTS = ("HTTPFound", "HTTPMovedPermanently", "HTTPSeeOther", "HTTPTemporaryRedirect", "HTTPPermanentRedirect")
26
44
  _CLIENT_FUNCTIONS = (
27
45
  *(f"aiohttp.ClientSession.{method}" for method in (*_METHODS, "request")),
@@ -40,18 +58,12 @@ class AiohttpModels(ModelPlugin):
40
58
  # The whole request is attacker-controlled: its query, match info, headers,
41
59
  # cookies and body all come from the client.
42
60
  *(TypedParameter(_sym(cls), "http") for cls in _REQUEST_CLASSES),
43
- # ``@routes.get('/')`` on a ``RouteTableDef``.
44
- *(EntryPoint(_sym(f"aiohttp.web.RouteTableDef.{method}"), "http") for method in (*_METHODS, "route", "view")),
45
- # ``app.router.add_route('GET', '/', handler)`` and ``app.router.add_get('/', handler)``.
46
- *(RouteRegistrar(_sym(f"{router}.add_route"), 2, "http", keyword="handler") for router in _ROUTERS),
61
+ *(EntryPoint(_sym(table), "http") for table in _ROUTE_TABLES),
62
+ *(RouteRegistrar(_sym(call), argument, "http", keyword="handler") for call, argument in _ROUTE_CALLS),
47
63
  *(
48
- RouteRegistrar(_sym(f"{router}.add_{method}"), 1, "http", keyword="handler")
49
- for router in _ROUTERS
50
- for method in (*_METHODS, "view")
64
+ RequestObject(_sym(symbol), _REQUEST_TEXT)
65
+ for symbol in (*_REQUEST_CLASSES, *_ROUTE_TABLES, *(call for call, _ in _ROUTE_CALLS))
51
66
  ),
52
- # ``app.add_routes([web.get('/', handler)])``: the route definitions name the handler.
53
- RouteRegistrar(_sym("aiohttp.web.route"), 2, "http", keyword="handler"),
54
- *(RouteRegistrar(_sym(f"aiohttp.web.{method}"), 1, "http", keyword="handler") for method in (*_METHODS, "view")),
55
67
  Sink(_sym("aiohttp.web.Response"), TaintKind.HTML),
56
68
  Sink(_sym("aiohttp.web.FileResponse"), TaintKind.PATH),
57
69
  *(Sink(_sym(f"aiohttp.web.{exception}"), TaintKind.REDIRECT) for exception in _REDIRECTS),
@@ -17,6 +17,7 @@ from coretrace_python.taint import (
17
17
  AuthorizationGuard,
18
18
  EntryPoint,
19
19
  Model,
20
+ RequestObject,
20
21
  RouteRegistrar,
21
22
  Sanitizer,
22
23
  Sink,
@@ -88,6 +89,23 @@ _AUTHORIZATION_DECORATORS = (
88
89
  )
89
90
 
90
91
 
92
+ # What a request object gives as text: the query string, form fields, headers, cookies
93
+ # and path. Its body, its files and REST framework's parsed ``data`` may hold a structure.
94
+ _REQUEST_TEXT = (
95
+ "GET", "POST", "COOKIES", "META", "headers", "path", "path_info", "method", "scheme", "encoding",
96
+ "content_type", "content_params", "resolver_match", "get_full_path", "get_full_path_info",
97
+ "build_absolute_uri", "get_host", "get_port", "get_signed_cookie", "query_params",
98
+ )
99
+ # Every input a request object comes from: an annotated parameter, a class-based view's
100
+ # method or ``self.request``, a decorated view, a view registered in a URL configuration.
101
+ _REQUEST_INPUTS = (
102
+ *_REQUEST_CLASSES,
103
+ *_VIEW_BASES,
104
+ *(f"{base}.request" for base in _VIEW_BASES),
105
+ *_VIEW_DECORATORS,
106
+ *_ROUTE_REGISTRARS,
107
+ )
108
+
91
109
  _TARGET_ONLY = ((TaintKind.REDIRECT, (0,)),)
92
110
 
93
111
 
@@ -103,6 +121,7 @@ class DjangoModels(ModelPlugin):
103
121
  # ``self.request`` in a class-based view: the attribute is inherited from the base.
104
122
  *(Source(_sym(f"{base}.request"), "http") for base in _VIEW_BASES),
105
123
  *(EntryPoint(_sym(decorator), "http") for decorator in _VIEW_DECORATORS),
124
+ *(RequestObject(_sym(symbol), _REQUEST_TEXT) for symbol in _REQUEST_INPUTS),
106
125
  Sink(_sym("django.db.connection.cursor.execute"), TaintKind.SQL | TaintKind.CREDENTIAL, ((TaintKind.SQL, (0,)),)),
107
126
  Sink(_sym("django.db.connection.cursor.executemany"), TaintKind.SQL | TaintKind.CREDENTIAL, ((TaintKind.SQL, (0,)),)),
108
127
  Sink(_sym("django.db.models.expressions.RawSQL"), TaintKind.SQL | TaintKind.CREDENTIAL),
@@ -8,7 +8,16 @@ from typing import ClassVar
8
8
 
9
9
  from coretrace_python.plugins import ModelPlugin
10
10
  from coretrace_python.semantic.symbols import SymbolId
11
- from coretrace_python.taint import TEXT_KINDS, EntryPoint, Model, Sanitizer, Sink, Source, TaintKind
11
+ from coretrace_python.taint import (
12
+ TEXT_KINDS,
13
+ EntryPoint,
14
+ Model,
15
+ RequestObject,
16
+ Sanitizer,
17
+ Sink,
18
+ Source,
19
+ TaintKind,
20
+ )
12
21
 
13
22
  _HANDLERS = ("tornado.web.RequestHandler", "tornado.websocket.WebSocketHandler")
14
23
  # Text, which cannot hold a query operator; the request object holds the raw body too.
@@ -16,6 +25,11 @@ _TEXT_METHODS = (
16
25
  "get_argument", "get_arguments", "get_query_argument", "get_query_arguments",
17
26
  "get_body_argument", "get_body_arguments", "get_cookie", "path_args", "path_kwargs",
18
27
  )
28
+ # What ``self.request`` gives as text; its body and files may hold a structure.
29
+ _REQUEST_TEXT = (
30
+ "arguments", "query_arguments", "body_arguments", "query", "uri", "path", "headers", "cookies",
31
+ "method", "host", "host_name", "remote_ip", "protocol", "version", "full_url",
32
+ )
19
33
  _CLIENTS = ("tornado.httpclient.AsyncHTTPClient.fetch", "tornado.httpclient.HTTPClient.fetch")
20
34
  _TARGET_ONLY = ((TaintKind.REDIRECT, (0,)),)
21
35
 
@@ -33,6 +47,7 @@ class TornadoModels(ModelPlugin):
33
47
  EntryPoint(_sym("tornado.websocket.WebSocketHandler"), "http"),
34
48
  *(Source(_sym(f"{handler}.{method}"), "http", TEXT_KINDS) for handler in _HANDLERS for method in _TEXT_METHODS),
35
49
  *(Source(_sym(f"{handler}.request"), "http") for handler in _HANDLERS),
50
+ *(RequestObject(_sym(f"{handler}.request"), _REQUEST_TEXT) for handler in _HANDLERS),
36
51
  *(Sink(_sym(f"{handler}.write"), TaintKind.HTML) for handler in _HANDLERS),
37
52
  *(Sink(_sym(f"{handler}.finish"), TaintKind.HTML) for handler in _HANDLERS),
38
53
  *(Sink(_sym(f"{handler}.redirect"), TaintKind.REDIRECT, _TARGET_ONLY) for handler in _HANDLERS),
@@ -1,7 +1,7 @@
1
1
  name = "ssrf"
2
2
  version = "1.0.0"
3
3
  plugin_api = ">=1,<2"
4
- requires = ["taint.flows", "findings.refutation"]
4
+ requires = ["taint.flows", "findings.refutation", "ir.ssa", "abstract.module_strings"]
5
5
  provides = ["vulnerability.ssrf"]
6
6
 
7
7
  [entrypoint]
@@ -0,0 +1,64 @@
1
+ """Server-side request forgery: attacker-controlled input reaching a SSRF sink.
2
+
3
+ How the URL is built, with ``+`` and f-strings, can prove part of its destination. Constant
4
+ text fixing ``scheme://host`` and ending the authority with ``/``, ``?`` or ``#`` before
5
+ any input keeps the input from choosing the host; the input still chooses the path, which
6
+ may reach a sensitive resource of that host, and a redirect may lead elsewhere, so the
7
+ flow is a hotspot. With the path fixed too, the input reaching the query or fragment
8
+ only, and redirects disabled in the call, the destination is fixed and the flow is
9
+ refuted. Anything unproven, a base of unknown value or a URL built in a helper function,
10
+ stays a vulnerability.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import re
16
+ from typing import ClassVar
17
+
18
+ from coretrace_python.abstract.strings import ModuleStringsAnalysis, leading_text
19
+ from coretrace_python.analysis import AnyAnalysis
20
+ from coretrace_python.findings import Severity
21
+ from coretrace_python.findings.refutation import Status, Verdict
22
+ from coretrace_python.hir import nodes
23
+ from coretrace_python.ir.ssa import SSAAnalysis
24
+ from coretrace_python.plugins import PluginContext, TaintDetector
25
+ from coretrace_python.taint import TaintFlow, TaintKind
26
+
27
+ # ``scheme://`` and an authority ended by ``/``, ``?`` or ``#``. A backslash, which some
28
+ # URL parsers take for a slash, does not end it here.
29
+ _FIXED_HOST = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*://[^/?#\\]+[/?#]")
30
+ # The same with the path fixed too: what follows is query or fragment.
31
+ _FIXED_PATH = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*://[^/?#\\]+(?:/[^?#]*)?[?#]")
32
+ # A client honours the one of these keywords it knows and rejects the other with a
33
+ # ``TypeError``: either way, the call follows no redirect.
34
+ _NO_REDIRECTS = ("allow_redirects", "follow_redirects")
35
+
36
+
37
+ class SsrfPlugin(TaintDetector):
38
+ name: ClassVar[str] = "ssrf"
39
+ rule_id: ClassVar[str] = "ssrf"
40
+ kind: ClassVar[TaintKind] = TaintKind.SSRF
41
+ severity: ClassVar[Severity] = Severity.HIGH
42
+ title: ClassVar[str] = "Server-side request forgery"
43
+ requires: ClassVar[frozenset[AnyAnalysis]] = TaintDetector.requires | {SSAAnalysis, ModuleStringsAnalysis}
44
+
45
+ def judge(self, ctx: PluginContext, function: nodes.Function, flow: TaintFlow, verdict: Verdict) -> Verdict:
46
+ if flow.through is not None:
47
+ return verdict # the URL is built in the callee
48
+ ssa = ctx.get(SSAAnalysis, function)
49
+ defs = {i.result: i for block in ssa.blocks for i in block.instructions if i.result is not None}
50
+ text, _ = leading_text(flow.argument, defs, ctx.get(ModuleStringsAnalysis))
51
+ host = _FIXED_HOST.match(text)
52
+ if host is None:
53
+ return verdict
54
+ arguments = flow.sink_arguments
55
+ no_redirects = arguments is not None and any(arguments.given(name) == (True, "False") for name in _NO_REDIRECTS)
56
+ if _FIXED_PATH.match(text) is not None:
57
+ if no_redirects:
58
+ return Verdict(flow, Status.REFUTED, f"the destination is fixed by {text!r} and redirects are disabled")
59
+ left = "a redirect may lead elsewhere"
60
+ else:
61
+ left = "the input chooses the path" + ("" if no_redirects else ", and a redirect may lead elsewhere")
62
+ if verdict.status is not Status.VULNERABILITY:
63
+ return verdict
64
+ return Verdict(flow, Status.HOTSPOT, f"the host is fixed by {host.group(0)!r}, but {left}")
@@ -17,6 +17,7 @@ from typing import Any, ClassVar
17
17
 
18
18
  from coretrace_python import __version__
19
19
  from coretrace_python.abstract import ConstantPropagation, HeapAnalysis, RangeAnalysis
20
+ from coretrace_python.abstract.strings import ModuleStringsAnalysis
20
21
  from coretrace_python.analysis import (
21
22
  AnalysisContext,
22
23
  AnalysisManager,
@@ -143,6 +144,7 @@ ALL_ANALYSES: tuple[AnyAnalysis, ...] = (
143
144
  DefUseAnalysis,
144
145
  ConstantPropagation,
145
146
  RangeAnalysis,
147
+ ModuleStringsAnalysis,
146
148
  HeapAnalysis,
147
149
  CallGraphAnalysis,
148
150
  SummaryAnalysis,
@@ -13,13 +13,14 @@ from typing import ClassVar
13
13
 
14
14
  from coretrace_python.analysis import AnyAnalysis
15
15
  from coretrace_python.findings import Confidence, Finding, Severity
16
- from coretrace_python.findings.refutation import RefutationAnalysis, Status
16
+ from coretrace_python.findings.refutation import RefutationAnalysis, Status, Verdict
17
+ from coretrace_python.hir import nodes
17
18
  from coretrace_python.interprocedural import CallGraphAnalysis
18
19
  from coretrace_python.ir.model import Call, Instruction, Symbol, Value
19
20
  from coretrace_python.ir.ssa import SSAAnalysis
20
21
  from coretrace_python.plugins.api import Plugin, PluginContext
21
22
  from coretrace_python.semantic.symbols import SymbolId
22
- from coretrace_python.taint import TaintAnalysis, TaintKind
23
+ from coretrace_python.taint import TaintAnalysis, TaintFlow, TaintKind
23
24
 
24
25
 
25
26
  class TaintDetector(Plugin):
@@ -40,6 +41,9 @@ class TaintDetector(Plugin):
40
41
  if not flow.kinds & self.kind:
41
42
  continue
42
43
  verdict = verdicts.verdict(flow)
44
+ if verdict.status is Status.REFUTED:
45
+ continue
46
+ verdict = self.judge(ctx, function, flow, verdict)
43
47
  if verdict.status is Status.REFUTED:
44
48
  continue
45
49
  message = f"{self.title}: {flow.source.label} input reaches {flow.sink.symbol}"
@@ -70,6 +74,13 @@ class TaintDetector(Plugin):
70
74
  )
71
75
  return findings
72
76
 
77
+ def judge(self, ctx: PluginContext, function: nodes.Function, flow: TaintFlow, verdict: Verdict) -> Verdict:
78
+ """This rule's verdict on ``flow``, given the refutation's: the same, unless the rule
79
+ knows more about its own sinks. The refutation's verdict, which other consumers
80
+ of the flow read, does not change."""
81
+
82
+ return verdict
83
+
73
84
 
74
85
  def _lower(confidence: Confidence) -> Confidence:
75
86
  return {Confidence.HIGH: Confidence.MEDIUM}.get(confidence, Confidence.LOW)
@@ -17,6 +17,7 @@ from coretrace_python.taint.models import (
17
17
  ModelError,
18
18
  ModelTable,
19
19
  NamedParameter,
20
+ RequestObject,
20
21
  RouteRegistrar,
21
22
  SafeArgument,
22
23
  Sanitizer,
@@ -45,6 +46,7 @@ __all__ = [
45
46
  "ModelTable",
46
47
  "NamedParameter",
47
48
  "RegisteredRoutes",
49
+ "RequestObject",
48
50
  "RouteRegistrar",
49
51
  "Routes",
50
52
  "SafeArgument",
@@ -74,6 +74,7 @@ from coretrace_python.semantic.scopes import ScopeAnalysis, ScopeId, ScopeTable
74
74
  from coretrace_python.semantic.symbols import SymbolAnalysis, SymbolId, SymbolTable
75
75
  from coretrace_python.source import SourceSpan
76
76
  from coretrace_python.taint.models import (
77
+ TEXT_KINDS,
77
78
  EntryPoint,
78
79
  ModelTable,
79
80
  SecurityModelAnalysis,
@@ -84,6 +85,9 @@ from coretrace_python.taint.models import (
84
85
  from coretrace_python.taint.routes import RegisteredRoutes, Routes
85
86
  from coretrace_python.taint.templates import EscapedTemplates
86
87
 
88
+ # What only a structure carries, and text does not.
89
+ _STRUCTURED = TaintKind.ALL & ~TEXT_KINDS
90
+
87
91
 
88
92
  @dataclass(frozen=True)
89
93
  class Taint:
@@ -274,19 +278,42 @@ class _TaintProblem(DataflowProblem[State]):
274
278
  taint = Taint.none()
275
279
  symbol = self.symbols.get(instruction.result) if instruction.result else None
276
280
  if symbol is not None:
277
- source = self.models.source_covering(symbol)
278
- if source is not None:
279
- taint = Taint(source.kinds, frozenset({source}))
281
+ taint = self.covered(symbol)
280
282
  if isinstance(instruction, Symbol | Compare):
281
283
  return taint
282
284
  for operand in instruction.operands():
283
- taint = taint.join(state.get(operand, Taint.none()))
285
+ operand_taint = state.get(operand, Taint.none())
286
+ if isinstance(instruction, GetAttr):
287
+ operand_taint = self.attribute(operand_taint, instruction.attribute)
288
+ taint = taint.join(operand_taint)
284
289
  if isinstance(instruction, BuildList | BuildTuple | BuildDict | BuildSet):
285
290
  # ``[*xs]`` and ``{**d}`` copy the contents of what they unpack.
286
291
  for unpacked in instruction.unpacked:
287
292
  taint = taint.join(self.deep(unpacked, state))
288
293
  return taint
289
294
 
295
+ def covered(self, symbol: SymbolId) -> Taint:
296
+ """The taint of the source covering ``symbol``, if any. Below a request source, as
297
+ ``self.request.GET`` is, it is the taint of the request attribute read."""
298
+
299
+ source = self.models.source_covering(symbol)
300
+ if source is None:
301
+ return Taint.none()
302
+ taint = Taint(source.kinds, frozenset({source}))
303
+ if symbol == source.symbol:
304
+ return taint
305
+ read = symbol.canonical_name.removeprefix(f"{source.symbol.canonical_name}.").split(".")[0]
306
+ return self.attribute(taint, read)
307
+
308
+ def attribute(self, taint: Taint, name: str) -> Taint:
309
+ """What attribute ``name`` of a value carrying ``taint`` carries: text when every
310
+ source of the value is a request object listing ``name`` among its text attributes."""
311
+
312
+ requests = [self.models.request_object(source.symbol) for source in taint.sources]
313
+ if taint and all(request is not None and name in request.text for request in requests):
314
+ return taint.without(_STRUCTURED)
315
+ return taint
316
+
290
317
  def call(self, call: Call, state: dict[Key, Taint], flows: list[TaintFlow]) -> Taint:
291
318
  arguments = tuple(self.deep(a, state) for a in call.arguments)
292
319
  keywords = Taint.none()
@@ -420,11 +447,7 @@ class _TaintProblem(DataflowProblem[State]):
420
447
  if cleared:
421
448
  return everything.without(TaintKind(cleared))
422
449
  # A method on a tainted object returns tainted data (``request.args.get``).
423
- everything = everything.join(state.get(call.callee, Taint.none()))
424
- source = self.models.source_covering(symbol)
425
- if source is not None:
426
- return everything.join(Taint(source.kinds, frozenset({source})))
427
- return everything
450
+ return everything.join(state.get(call.callee, Taint.none())).join(self.covered(symbol))
428
451
 
429
452
  def guarded(self, sink: Sink | None, arguments: Arguments) -> Sink | None:
430
453
  """The sink as this call makes it: an argument declared safe (``yaml.load`` with
@@ -777,7 +800,8 @@ def parameter_sources(
777
800
  entry point (``self`` excepted for a method), including one registered elsewhere
778
801
  (``routes``), and every parameter annotated with a typed-parameter symbol. An entry
779
802
  point's parameter annotated with scalars is text: FastAPI validates it before the
780
- handler runs, and a scalar cannot hold a query operator."""
803
+ handler runs, and a scalar cannot hold a query operator. So are the URL parameters
804
+ a view receives after a request object."""
781
805
 
782
806
  owner = _owner(module, function)
783
807
  sources: dict[int, Source] = {}
@@ -786,10 +810,13 @@ def parameter_sources(
786
810
  entry = function_entry_point(function, module, models, scopes, symbols, instances, routes)
787
811
  if entry is not None:
788
812
  first = 1 if owner is not None else 0
813
+ request = models.request_object(entry.symbol)
789
814
  for index in range(first, len(function.parameters)):
790
815
  annotation = function.parameters[index].annotation
791
- scalar = annotation is not None and _scalar(annotation, symbols, enclosing)
792
- kinds = entry.kinds & ~TaintKind.NOSQL if scalar else entry.kinds
816
+ text = (request is not None and index > first) or (
817
+ annotation is not None and _scalar(annotation, symbols, enclosing)
818
+ )
819
+ kinds = entry.kinds & ~_STRUCTURED if text else entry.kinds
793
820
  sources[index] = Source(entry.symbol, entry.label, kinds)
794
821
  for index, parameter in enumerate(function.parameters):
795
822
  if parameter.annotation is None: