|
1 | 1 | { |
2 | 2 | "_meta": { |
3 | | - "version": "1.3.0", |
| 3 | + "version": "1.7.0", |
4 | 4 | "description": "SAP attack-path templates. CONTENT, not code: a new path is a new entry here, never a code change. Each template names the checks that evidence each hop, so every hop is grounded in a check that already exists and a path can never claim more than the scanner actually detected.", |
5 | 5 | "how_a_path_instantiates": "Every REQUIRED hop must have at least one OPEN finding in the landscape. Optional hops enrich a path but do not gate it. A path is therefore a statement that these conditions CO-EXIST, derived from exported configuration — never a claim that anything was traversed or validated. The console must say so.", |
6 | 6 | "cut_semantics": "A hop marked cut=true appears on every variant of the path, so closing it disconnects the path. Non-cut hops reduce exploitability without severing it. This is the mitigate-vs-additional split: it needs no clever algorithm, only that each hop knows whether removing it disconnects.", |
|
18 | 18 | "SAP Secure Operations Map — transport protection as a control in its own right", |
19 | 19 | "SAP Security Notes / HotNews — Priority 1 and actively-exploited advisories", |
20 | 20 | "SAP Help — S/4HANA business roles, business catalogs and CDS access control", |
21 | | - "SAP Help — Communication arrangements and communication users in RISE" |
22 | | - ] |
| 21 | + "SAP Help — Communication arrangements and communication users in RISE", |
| 22 | + "SAP Help — CAP authorization (@requires/@restrict) and XSUAA security descriptors" |
| 23 | + ], |
| 24 | + "uncited_by_design": { |
| 25 | + "CSA-*": "SAP's own compliance verdicts, imported and reported as SAP's. They are somebody else's finding rather than our evidence, and a hop that cited them would make the path depend on whether the customer licenses Focused Run — the same estate would instantiate different paths according to its SAP contract. modules/cloudalm_verdicts.py reports them beside our own verdicts for comparison, which is the right place for them." |
| 26 | + } |
23 | 27 | }, |
24 | 28 | "paths": [ |
25 | 29 | { |
|
631 | 635 | "checks": [ |
632 | 636 | "HANADB-PARAM-003", |
633 | 637 | "HANADB-PARAM-005", |
634 | | - "CRYPTO-HANA-005", |
635 | | - "HANADB-PARAM-004" |
| 638 | + "CRYPTO-HANA-005" |
636 | 639 | ], |
637 | 640 | "node_types": [ |
638 | 641 | "parameter_name" |
639 | 642 | ], |
640 | | - "note": "Cross-database (MDC) access matters here specifically: it turns a tenant boundary into a reachable one, so a principal in a non-production tenant is adjacent to the production tenant's data." |
| 643 | + "note": "HANADB-PARAM-004 (log_mode) was cited here and has moved to SAPPATH-22. It is a recovery setting and this hop is about the channel; it sat here because the library had no path about recovery to put it on." |
641 | 644 | }, |
642 | 645 | { |
643 | 646 | "name": "The read would not be recorded", |
|
910 | 913 | "CRYPTO-CERT-003", |
911 | 914 | "CRYPTO-KEY-001", |
912 | 915 | "BTP-CC-005", |
913 | | - "BTP-CC-006" |
| 916 | + "BTP-CC-006", |
| 917 | + "CRYPTO-ECS-001" |
914 | 918 | ], |
915 | 919 | "node_types": [ |
916 | 920 | "certificate", |
917 | 921 | "cc_backend" |
918 | 922 | ], |
919 | | - "note": "An expired certificate usually presents as an outage rather than a breach — its risk here is the workaround it provokes, because verification switched off to restore an interface is rarely switched back on." |
| 923 | + "note": "CRYPTO-ECS-001 is the one that undoes the rest: SSF_PSE_D holds the system's private keys, and a key read out of a table is a key however well the channel using it is configured. Every other check on this hop is about whether the anchors are sound; this one is about whether they are still only yours." |
920 | 924 | }, |
921 | 925 | { |
922 | 926 | "name": "The credential unlocks a trust relationship", |
|
1219 | 1223 | "CODE-INJ-001", |
1220 | 1224 | "CODE-INJ-003", |
1221 | 1225 | "CODE-STMT-001", |
1222 | | - "CODE-MOD-002" |
| 1226 | + "CODE-MOD-002", |
| 1227 | + "ABAP-CINJ-001", |
| 1228 | + "ABAP-CINJ-002", |
| 1229 | + "ABAP-CINJ-007", |
| 1230 | + "ABAP-CINJ-011", |
| 1231 | + "ABAP-CMDI-001", |
| 1232 | + "ABAP-SQLI-001", |
| 1233 | + "ABAP-SQLI-002", |
| 1234 | + "ABAP-SQLI-003", |
| 1235 | + "ABAP-SQLI-004", |
| 1236 | + "ABAP-SQLI-009" |
1223 | 1237 | ], |
1224 | 1238 | "node_types": [ |
1225 | 1239 | "program" |
1226 | 1240 | ], |
1227 | | - "why_cut": "Fixing the statement removes the defect, and nothing else does. A parameter can narrow who reaches a program; none can make a concatenated SQL string safe. Note that both this scanner's findings and SAP's own ATC results appear here — where the customer already licenses ATC, its verdict is authoritative and duplicating it would only disagree with SAP about SAP's own tool." |
| 1241 | + "why_cut": "Fixing the statement removes the defect, and nothing else does. A parameter can narrow who reaches a program; none can make a concatenated SQL string safe. Note that both this scanner's findings and SAP's own ATC results appear here — where the customer already licenses ATC, its verdict is authoritative and duplicating it would only disagree with SAP about SAP's own tool.", |
| 1242 | + "note": "Both this product's own scanner and SAP's ATC results appear here, and the two are not duplicates: `ATC-*` is SAP's verdict where the customer licenses ATC, and `ABAP-*` is what this scanner found in the abapGit export. Where both are present ATC is authoritative — duplicating it would only disagree with SAP about SAP's own tool. The `ABAP-*` ids cited are the CRITICAL-severity rules of the injection families; the scanner represents each rule as a check while ATC represents each family as one, so citing every rule would put fifty-nine ids on this hop." |
1228 | 1243 | }, |
1229 | 1244 | { |
1230 | 1245 | "name": "Nothing checks the caller's authority", |
|
1234 | 1249 | "ATC-AUTHCHK", |
1235 | 1250 | "CODE-INJ-002", |
1236 | 1251 | "AUTH-001", |
1237 | | - "BASELINE-002" |
| 1252 | + "BASELINE-002", |
| 1253 | + "ABAP-AUTH-001", |
| 1254 | + "ABAP-AUTH-002", |
| 1255 | + "ABAP-AUTH-003", |
| 1256 | + "ABAP-AUTH-004", |
| 1257 | + "ABAP-AUTH-005", |
| 1258 | + "ABAP-AUTH-006", |
| 1259 | + "ABAP-AUTH-008", |
| 1260 | + "ABAP-BKDR-001", |
| 1261 | + "ABAP-BKDR-002", |
| 1262 | + "ABAP-BKDR-003", |
| 1263 | + "ABAP-BKDR-004", |
| 1264 | + "ABAP-BKDR-006" |
1238 | 1265 | ], |
1239 | 1266 | "node_types": [ |
1240 | 1267 | "program", |
1241 | 1268 | "auth_object", |
1242 | 1269 | "parameter_name" |
1243 | 1270 | ], |
1244 | | - "why_cut": "An AUTHORITY-CHECK in the program means the caller must hold the object before the defect is reachable at all. It severs the path without fixing the defect, which is what makes it worth having: it is a change a Basis team can transport in days, where the code fix waits on the team that owns the program." |
| 1271 | + "why_cut": "An AUTHORITY-CHECK in the program means the caller must hold the object before the defect is reachable at all. It severs the path without fixing the defect, which is what makes it worth having: it is a change a Basis team can transport in days, where the code fix waits on the team that owns the program.", |
| 1272 | + "note": "The backdoor rules sit here rather than on the hop above because that is what they are: a hardcoded user comparison or a hidden bypass is an authority check that was written to pass, not an injectable defect. Cited at CRITICAL and HIGH only, by the same rule as the hop above." |
1245 | 1273 | }, |
1246 | 1274 | { |
1247 | 1275 | "name": "The program is reachable from outside", |
|
1469 | 1497 | "PARAM-login/password_downwards_compatibility", |
1470 | 1498 | "BASELINE-005", |
1471 | 1499 | "BASELINE-011", |
1472 | | - "BASELINE-004" |
| 1500 | + "BASELINE-004", |
| 1501 | + "AUTH-ECS-001" |
1473 | 1502 | ], |
1474 | 1503 | "node_types": [ |
1475 | 1504 | "parameter_name" |
1476 | 1505 | ], |
1477 | | - "why_cut": "A lockout threshold turns an unlimited number of attempts into a handful, which is what makes a weak password survivable. Note this hop cites the PARAM-* family rather than its twins elsewhere: these particular settings have no second representation, unlike gw/sec_info or rsau/enable, which arrive twice and are cited once." |
| 1506 | + "why_cut": "A lockout threshold turns an unlimited number of attempts into a handful, which is what makes a weak password survivable. Note this hop cites the PARAM-* family rather than its twins elsewhere: these particular settings have no second representation, unlike gw/sec_info or rsau/enable, which arrive twice and are cited once. AUTH-ECS-001 changes what this hop is about where it fires: with the password-hash tables not behind authorization group SPWD, guessing moves OFFLINE, and offline guessing is bounded by nothing on this list — no lockout, no logon record, nothing to alert on. Assigning the group is what keeps the attack in the bounded case these parameters govern." |
1478 | 1507 | }, |
1479 | 1508 | { |
1480 | 1509 | "name": "Somebody is already trying", |
|
1733 | 1762 | "note": "Server headers, detailed errors and unredacted traces are reconnaissance rather than access. On their own they are hygiene; behind an edge that does not insist on TLS they tell an attacker which version to target." |
1734 | 1763 | } |
1735 | 1764 | ] |
| 1765 | + }, |
| 1766 | + { |
| 1767 | + "id": "SAPPATH-22", |
| 1768 | + "name": "Recovery is not assured", |
| 1769 | + "summary": "No recent full backup, no log chain, no tested restore, and backups that are readable by whoever holds them.", |
| 1770 | + "narrative": "Every other template in this library describes somebody reaching something. This one describes what is true afterwards, and it needs no attacker of its own — it is the multiplier on all twenty-one of them. A destructive event has one answer that makes it survivable: restore to just before it. These conditions remove that answer, and they remove it quietly, because a backup chain that is not working looks exactly like one that is until somebody tries. It is also the path an attacker most wants to find. Ransomware economics are entirely about whether the target can roll back, and a destructive insider needs the same thing to be true. The last hop is the one that surprises people: unencrypted backups mean the recovery chain is also a disclosure route, so the same weakness that leaves you unable to recover leaves a complete copy of the business readable by anyone who reaches the storage.", |
| 1771 | + "fair_scenario": "SAP-DATA-04", |
| 1772 | + "severity": "HIGH", |
| 1773 | + "crosses_tier": false, |
| 1774 | + "hops": [ |
| 1775 | + { |
| 1776 | + "name": "There is no recent base to restore from", |
| 1777 | + "required": true, |
| 1778 | + "cut": true, |
| 1779 | + "checks": [ |
| 1780 | + "RES-BCK-001", |
| 1781 | + "RES-BCK-002", |
| 1782 | + "RES-JOB-001" |
| 1783 | + ], |
| 1784 | + "node_types": [], |
| 1785 | + "why_cut": "A full data backup is the base of the chain and everything else replays onto it, so re-establishing one severs this path at its root. It is also the cheapest cut in the library by a wide margin — a schedule and a destination, no design decision and no conversation with a business owner. RES-BCK-002 belongs on the same hop because an intermittent chain is not a working one: the recovery point is whatever the last SUCCESSFUL run was, and nobody knows that number without reading the catalogue run by run." |
| 1786 | + }, |
| 1787 | + { |
| 1788 | + "name": "Nothing bridges the gap since that base", |
| 1789 | + "required": false, |
| 1790 | + "cut": true, |
| 1791 | + "checks": [ |
| 1792 | + "RES-BCK-003", |
| 1793 | + "HANADB-PARAM-004" |
| 1794 | + ], |
| 1795 | + "node_types": [ |
| 1796 | + "parameter_name" |
| 1797 | + ], |
| 1798 | + "why_cut": "Log backups turn a set of discrete restore points into a continuous one, so scheduling them collapses the exposure from a day to an interval you choose. HANADB-PARAM-004 is cited here rather than on the database path where it used to sit: log_mode = overwrite is not a channel weakness, it is the configuration that makes log backups impossible, and it has to be fixed before RES-BCK-003 can be." |
| 1799 | + }, |
| 1800 | + { |
| 1801 | + "name": "Nobody has proved the chain works", |
| 1802 | + "required": false, |
| 1803 | + "cut": true, |
| 1804 | + "checks": [ |
| 1805 | + "RES-DR-001", |
| 1806 | + "RES-DR-002", |
| 1807 | + "RES-DR-003" |
| 1808 | + ], |
| 1809 | + "node_types": [], |
| 1810 | + "why_cut": "A restore test is the only control on this path that cannot be satisfied by configuration, and it is the one that finds what the others cannot: an encryption key held only on the system being restored, a chain missing a segment, a procedure that assumes somebody who has left. RES-DR-003 is the most useful member — a test that RAN and missed its objective has already told you the number the business plans against is wrong, which is worth more than a test that passed." |
| 1811 | + }, |
| 1812 | + { |
| 1813 | + "name": "The backups are readable by whoever holds them", |
| 1814 | + "required": false, |
| 1815 | + "cut": false, |
| 1816 | + "checks": [ |
| 1817 | + "CRYPTO-HANA-004" |
| 1818 | + ], |
| 1819 | + "node_types": [], |
| 1820 | + "note": "The hop that surprises people. Backup encryption is keyed independently of data-volume encryption, so an estate that encrypted at rest has usually not encrypted its backups — and a backup is the whole database in a file, outside every control the source system applies. Not a cut, because encrypting the backups does not restore the ability to recover; it changes what a copy is worth to somebody who takes one, which is a different loss on the same weakness." |
| 1821 | + }, |
| 1822 | + { |
| 1823 | + "name": "The evidence cannot settle the question", |
| 1824 | + "required": false, |
| 1825 | + "cut": false, |
| 1826 | + "checks": [ |
| 1827 | + "RES-BCK-004", |
| 1828 | + "RES-EVD-001" |
| 1829 | + ], |
| 1830 | + "node_types": [], |
| 1831 | + "note": "Not a weakness — a boundary, and the only hop of its kind in the library. Recovery is the subject where 'we found nothing' and 'we could not look' are most easily confused, because nobody exercises the capability until the worst day. Where these fire, the hops above are unanswered rather than clean, and the honest reading of this path is that it may or may not be open." |
| 1832 | + } |
| 1833 | + ] |
| 1834 | + }, |
| 1835 | + { |
| 1836 | + "id": "SAPPATH-23", |
| 1837 | + "name": "The CAP application is the reachable surface", |
| 1838 | + "summary": "A CAP service that checks nothing, a role collection granted by birthright, and a token anybody can obtain.", |
| 1839 | + "narrative": "A RISE estate is not only the ABAP stack. Custom applications built on the Cloud Application Programming Model run on BTP beside it, hold their own business data, and are governed by an entirely different authorization model — XSUAA scopes and role collections rather than authorization objects. This product ships fifteen checks for that surface and, until this template, cited none of them on any path: the module reads a project directory that no upload could supply, so there was nothing to cite. The route is short and needs no exploitation. A CAP service without `@requires` answers any authenticated caller; a role collection granted to every federated user by birthright makes 'authenticated' mean 'anybody in the corporate directory'; and a wildcard redirect URI decides how hard the token is to obtain in the first place. Read this beside SAPPATH-19, which is the same shape one stack over: that one is ABAP Fiori and OData, this one is the application the customer wrote themselves.", |
| 1840 | + "fair_scenario": "SAP-DATA-04", |
| 1841 | + "severity": "HIGH", |
| 1842 | + "crosses_tier": true, |
| 1843 | + "hops": [ |
| 1844 | + { |
| 1845 | + "name": "A CAP service checks no authorization", |
| 1846 | + "required": true, |
| 1847 | + "cut": true, |
| 1848 | + "checks": [ |
| 1849 | + "CAPX-CDS-001", |
| 1850 | + "CAPX-CDS-002", |
| 1851 | + "CAPX-CDS-003" |
| 1852 | + ], |
| 1853 | + "node_types": [ |
| 1854 | + "cap_service" |
| 1855 | + ], |
| 1856 | + "why_cut": "The service is the last thing between a request and the data, so a `@requires` there ends the path whatever the role model above it allows. CAPX-CDS-003 is the one worth reading twice: a model enforcing a role no security descriptor grants is not protected but broken — the annotation names something that cannot be held, so either nobody can use the service or the check does not bite, and both are worse than the author intended." |
| 1857 | + }, |
| 1858 | + { |
| 1859 | + "name": "The role that reaches it is granted by birthright", |
| 1860 | + "required": false, |
| 1861 | + "cut": true, |
| 1862 | + "checks": [ |
| 1863 | + "CAPX-GRAPH-002", |
| 1864 | + "CAPX-SCOPE-001", |
| 1865 | + "CAPX-AUTH-001", |
| 1866 | + "BTP-IAS-005", |
| 1867 | + "BTP-IAS-004" |
| 1868 | + ], |
| 1869 | + "node_types": [ |
| 1870 | + "xsuaa_application", |
| 1871 | + "role_collection" |
| 1872 | + ], |
| 1873 | + "why_cut": "Withdrawing a birthright grant narrows who can reach the service without touching the service, and it is the faster of the two cuts because it is a cockpit change rather than a code change and a deployment. CAPX-AUTH-001 belongs here because an application that accepts every authority granted to it, without naming them, cannot be reasoned about at all: what it can do is whatever it was last given." |
| 1874 | + }, |
| 1875 | + { |
| 1876 | + "name": "The token is easy to obtain", |
| 1877 | + "required": false, |
| 1878 | + "cut": true, |
| 1879 | + "checks": [ |
| 1880 | + "CAPX-URI-001", |
| 1881 | + "CAPX-TOK-001", |
| 1882 | + "CAPX-TEN-001", |
| 1883 | + "CAPX-CRED-001" |
| 1884 | + ], |
| 1885 | + "node_types": [ |
| 1886 | + "xsuaa_application", |
| 1887 | + "url" |
| 1888 | + ], |
| 1889 | + "why_cut": "Naming the redirect host rather than accepting a wildcard closes the authorization-code interception this hop is about. The other three lengthen the window rather than opening it: a token policy overriding the subaccount's, a shared tenant mode putting one client secret everywhere, and an instance secret that cannot be rotated all mean a credential, once obtained, stays useful." |
| 1890 | + }, |
| 1891 | + { |
| 1892 | + "name": "The restriction that exists is not enforced", |
| 1893 | + "required": false, |
| 1894 | + "cut": false, |
| 1895 | + "checks": [ |
| 1896 | + "CAPX-ATTR-001", |
| 1897 | + "CAPX-CDS-004", |
| 1898 | + "CAPX-GRAPH-001" |
| 1899 | + ], |
| 1900 | + "node_types": [ |
| 1901 | + "cap_service" |
| 1902 | + ], |
| 1903 | + "note": "The hop where the model looks right and the runtime disagrees. CAP evaluates authorization on the request target only, so a single `$expand` reaches a restricted entity through an unrestricted one — documented behaviour rather than a defect, which is why it cannot be waited out. `valueRequired=false` on an attribute restriction is the same shape: the restriction is declared and does not constrain." |
| 1904 | + }, |
| 1905 | + { |
| 1906 | + "name": "What it returns is personal", |
| 1907 | + "required": false, |
| 1908 | + "cut": false, |
| 1909 | + "checks": [ |
| 1910 | + "CAPX-CDS-005", |
| 1911 | + "DPP-FIELD-003", |
| 1912 | + "DPP-RAL-002" |
| 1913 | + ], |
| 1914 | + "node_types": [ |
| 1915 | + "cap_service" |
| 1916 | + ], |
| 1917 | + "note": "CAP has no property-level authorization, so the projection IS the access control for these fields: whoever may read the entity reads every column of it. Shares its ending with the other data paths, and for the same reason — a read is not a change, so nothing in the change log sees it." |
| 1918 | + } |
| 1919 | + ] |
1736 | 1920 | } |
1737 | 1921 | ] |
1738 | 1922 | } |
0 commit comments