Fix bypass of IP restriction allowing access to restricted group snippets
What does this MR do and why?
Related to: Bypassing "Restrict access by IP address" to vi... (#499487 - closed)
This MR aims to prevent users from viewing snippet names belonging to restricted group projects if their IP address is not authorized. This ensures that IP-based access control is enforced consistently for snippet visibility.
To help you better understand, this is what the filter_unauthorised_snippets method does:
It excludes unauthorized snippets from the list of snippets it receives as an argument.
The Query Plans: The Postgres.io link
Sort (cost=6129.73..6129.74 rows=6 width=2129) (actual time=1.328..1.337 rows=52 loops=1)
Sort Key: snippets.id DESC
Sort Method: quicksort Memory: 76kB
Buffers: shared hit=1728
I/O Timings: read=0.000 write=0.000
-> Nested Loop (cost=874.54..6129.65 rows=6 width=2129) (actual time=1.060..1.256 rows=52 loops=1)
Buffers: shared hit=1725
I/O Timings: read=0.000 write=0.000
-> HashAggregate (cost=400.11..400.23 rows=12 width=4) (actual time=0.496..0.505 rows=52 loops=1)
Group Key: snippets_1.id
Buffers: shared hit=608
I/O Timings: read=0.000 write=0.000
-> Append (cost=0.42..400.08 rows=12 width=4) (actual time=0.019..0.479 rows=67 loops=1)
Buffers: shared hit=608
I/O Timings: read=0.000 write=0.000
-> Index Scan using index_snippets_on_author_id on public.snippets snippets_1 (cost=0.42..82.79 rows=8 width=4) (actual time=0.018..0.096 rows=33 loops=1)
Index Cond: (snippets_1.author_id = 64248)
Filter: ((snippets_1.project_id IS NULL) AND (snippets_1.visibility_level = ANY ('{10,20}'::integer[])))
Rows Removed by Filter: 24
Buffers: shared hit=60
I/O Timings: read=0.000 write=0.000
-> Nested Loop (cost=1.42..114.00 rows=3 width=4) (actual time=0.033..0.154 rows=15 loops=1)
Buffers: shared hit=208
I/O Timings: read=0.000 write=0.000
-> Nested Loop (cost=0.86..111.59 rows=2 width=12) (actual time=0.021..0.099 rows=16 loops=1)
Buffers: shared hit=128
I/O Timings: read=0.000 write=0.000
-> Index Scan using index_snippets_on_author_id on public.snippets snippets_2 (cost=0.42..82.79 rows=14 width=8) (actual time=0.007..0.032 rows=49 loops=1)
Index Cond: (snippets_2.author_id = 64248)
Filter: (snippets_2.visibility_level = ANY ('{10,20}'::integer[]))
Rows Removed by Filter: 8
Buffers: shared hit=60
I/O Timings: read=0.000 write=0.000
-> Index Only Scan using index_projects_on_id_partial_for_visibility on public.projects (cost=0.43..2.06 rows=1 width=4) (actual time=0.001..0.001 rows=0 loops=49)
Index Cond: (projects.id = snippets_2.project_id)
Heap Fetches: 15
Buffers: shared hit=68
I/O Timings: read=0.000 write=0.000
-> Index Scan using index_project_features_on_project_id on public.project_features (cost=0.56..1.21 rows=1 width=4) (actual time=0.003..0.003 rows=1 loops=16)
Index Cond: (project_features.project_id = projects.id)
Filter: (project_features.snippets_access_level = ANY ('{20,30}'::integer[]))
Rows Removed by Filter: 0
Buffers: shared hit=80
I/O Timings: read=0.000 write=0.000
-> Nested Loop (cost=2.13..203.23 rows=1 width=4) (actual time=0.042..0.222 rows=19 loops=1)
Buffers: shared hit=340
I/O Timings: read=0.000 write=0.000
-> Nested Loop (cost=1.57..202.56 rows=1 width=16) (actual time=0.036..0.171 rows=19 loops=1)
Buffers: shared hit=245
I/O Timings: read=0.000 write=0.000
-> Nested Loop Semi Join (cost=1.00..200.49 rows=1 width=12) (actual time=0.026..0.093 rows=19 loops=1)
Buffers: shared hit=143
I/O Timings: read=0.000 write=0.000
-> Index Scan using index_snippets_on_author_id on public.snippets snippets_3 (cost=0.42..82.65 rows=54 width=8) (actual time=0.007..0.028 rows=57 loops=1)
Index Cond: (snippets_3.author_id = 64248)
Buffers: shared hit=60
I/O Timings: read=0.000 write=0.000
-> Index Only Scan using project_authorizations_pkey on public.project_authorizations (cost=0.58..2.15 rows=1 width=4) (actual time=0.001..0.001 rows=0 loops=57)
Index Cond: ((project_authorizations.user_id = 21572755) AND (project_authorizations.project_id = snippets_3.project_id))
Heap Fetches: 0
Buffers: shared hit=83
I/O Timings: read=0.000 write=0.000
-> Index Only Scan using projects_pkey on public.projects projects_1 (cost=0.56..2.06 rows=1 width=4) (actual time=0.003..0.003 rows=1 loops=19)
Index Cond: (projects_1.id = project_authorizations.project_id)
Heap Fetches: 18
Buffers: shared hit=102
I/O Timings: read=0.000 write=0.000
-> Index Scan using index_project_features_on_project_id on public.project_features project_features_1 (cost=0.56..0.66 rows=1 width=4) (actual time=0.002..0.002 rows=1 loops=19)
Index Cond: (project_features_1.project_id = projects_1.id)
Filter: (project_features_1.snippets_access_level = ANY ('{20,30,10}'::integer[]))
Rows Removed by Filter: 0
Buffers: shared hit=95
I/O Timings: read=0.000 write=0.000
-> Index Scan using index_snippets_on_id_and_type on public.snippets (cost=474.43..477.45 rows=1 width=2129) (actual time=0.014..0.014 rows=1 loops=52)
Index Cond: (snippets.id = snippets_1.id)
Filter: (NOT (hashed SubPlan 2))
Rows Removed by Filter: 0
Buffers: shared hit=1117
I/O Timings: read=0.000 write=0.000
SubPlan 2
-> Nested Loop (cost=401.95..474.00 rows=1 width=4) (actual time=0.543..0.546 rows=0 loops=1)
Buffers: shared hit=909
I/O Timings: read=0.000 write=0.000
-> Nested Loop (cost=401.38..446.41 rows=1 width=12) (actual time=0.543..0.545 rows=0 loops=1)
Buffers: shared hit=909
I/O Timings: read=0.000 write=0.000
-> Nested Loop (cost=401.10..438.94 rows=5 width=8) (actual time=0.322..0.521 rows=19 loops=1)
Buffers: shared hit=871
I/O Timings: read=0.000 write=0.000
-> Nested Loop (cost=400.53..425.07 rows=5 width=8) (actual time=0.316..0.481 rows=19 loops=1)
Buffers: shared hit=776
I/O Timings: read=0.000 write=0.000
-> HashAggregate (cost=400.11..400.23 rows=12 width=4) (actual time=0.305..0.312 rows=52 loops=1)
Group Key: snippets_5.id
Buffers: shared hit=608
I/O Timings: read=0.000 write=0.000
-> Append (cost=0.42..400.08 rows=12 width=4) (actual time=0.007..0.291 rows=67 loops=1)
Buffers: shared hit=608
I/O Timings: read=0.000 write=0.000
-> Index Scan using index_snippets_on_author_id on public.snippets snippets_5 (cost=0.42..82.79 rows=8 width=4) (actual time=0.006..0.027 rows=33 loops=1)
Index Cond: (snippets_5.author_id = 64248)
Filter: ((snippets_5.project_id IS NULL) AND (snippets_5.visibility_level = ANY ('{10,20}'::integer[])))
Rows Removed by Filter: 24
Buffers: shared hit=60
I/O Timings: read=0.000 write=0.000
-> Nested Loop (cost=1.42..114.00 rows=3 width=4) (actual time=0.019..0.100 rows=15 loops=1)
Buffers: shared hit=208
I/O Timings: read=0.000 write=0.000
-> Nested Loop (cost=0.86..111.59 rows=2 width=12) (actual time=0.013..0.068 rows=16 loops=1)
Buffers: shared hit=128
I/O Timings: read=0.000 write=0.000
-> Index Scan using index_snippets_on_author_id on public.snippets snippets_6 (cost=0.42..82.79 rows=14 width=8) (actual time=0.005..0.028 rows=49 loops=1)
Index Cond: (snippets_6.author_id = 64248)
Filter: (snippets_6.visibility_level = ANY ('{10,20}'::integer[]))
Rows Removed by Filter: 8
Buffers: shared hit=60
I/O Timings: read=0.000 write=0.000
-> Index Only Scan using index_projects_on_id_partial_for_visibility on public.projects projects_3 (cost=0.43..2.06 rows=1 width=4) (actual time=0.001..0.001 rows=0 loops=49)
Index Cond: (projects_3.id = snippets_6.project_id)
Heap Fetches: 15
Buffers: shared hit=68
I/O Timings: read=0.000 write=0.000
-> Index Scan using index_project_features_on_project_id on public.project_features project_features_2 (cost=0.56..1.21 rows=1 width=4) (actual time=0.002..0.002 rows=1 loops=16)
Index Cond: (project_features_2.project_id = projects_3.id)
Filter: (project_features_2.snippets_access_level = ANY ('{20,30}'::integer[]))
Rows Removed by Filter: 0
Buffers: shared hit=80
I/O Timings: read=0.000 write=0.000
-> Nested Loop (cost=2.13..203.23 rows=1 width=4) (actual time=0.028..0.158 rows=19 loops=1)
Buffers: shared hit=340
I/O Timings: read=0.000 write=0.000
-> Nested Loop (cost=1.57..202.56 rows=1 width=16) (actual time=0.022..0.119 rows=19 loops=1)
Buffers: shared hit=245
I/O Timings: read=0.000 write=0.000
-> Nested Loop Semi Join (cost=1.00..200.49 rows=1 width=12) (actual time=0.015..0.076 rows=19 loops=1)
Buffers: shared hit=143
I/O Timings: read=0.000 write=0.000
-> Index Scan using index_snippets_on_author_id on public.snippets snippets_7 (cost=0.42..82.65 rows=54 width=8) (actual time=0.005..0.027 rows=57 loops=1)
Index Cond: (snippets_7.author_id = 64248)
Buffers: shared hit=60
I/O Timings: read=0.000 write=0.000
-> Index Only Scan using project_authorizations_pkey on public.project_authorizations project_authorizations_1 (cost=0.58..2.15 rows=1 width=4) (actual time=0.001..0.001 rows=0 loops=57)
Index Cond: ((project_authorizations_1.user_id = 21572755) AND (project_authorizations_1.project_id = snippets_7.project_id))
Heap Fetches: 0
Buffers: shared hit=83
I/O Timings: read=0.000 write=0.000
-> Index Only Scan using projects_pkey on public.projects projects_4 (cost=0.56..2.06 rows=1 width=4) (actual time=0.002..0.002 rows=1 loops=19)
Index Cond: (projects_4.id = project_authorizations_1.project_id)
Heap Fetches: 18
Buffers: shared hit=102
I/O Timings: read=0.000 write=0.000
-> Index Scan using index_project_features_on_project_id on public.project_features project_features_3 (cost=0.56..0.66 rows=1 width=4) (actual time=0.002..0.002 rows=1 loops=19)
Index Cond: (project_features_3.project_id = projects_4.id)
Filter: (project_features_3.snippets_access_level = ANY ('{20,30,10}'::integer[]))
Rows Removed by Filter: 0
Buffers: shared hit=95
I/O Timings: read=0.000 write=0.000
-> Index Only Scan using index_snippet_on_id_and_project_id on public.snippets snippets_4 (cost=0.42..2.07 rows=1 width=8) (actual time=0.003..0.003 rows=0 loops=52)
Index Cond: ((snippets_4.id = snippets_5.id) AND (snippets_4.project_id IS NOT NULL))
Heap Fetches: 0
Buffers: shared hit=168
I/O Timings: read=0.000 write=0.000
-> Index Scan using projects_pkey on public.projects projects_2 (cost=0.56..2.77 rows=1 width=8) (actual time=0.002..0.002 rows=1 loops=19)
Index Cond: (projects_2.id = snippets_4.project_id)
Buffers: shared hit=95
I/O Timings: read=0.000 write=0.000
-> Index Only Scan using index_ip_restrictions_on_group_id on public.ip_restrictions ip_restrictions_1 (cost=0.29..1.06 rows=44 width=4) (actual time=0.001..0.001 rows=0 loops=19)
Index Cond: (ip_restrictions_1.group_id = projects_2.namespace_id)
Heap Fetches: 0
Buffers: shared hit=38
I/O Timings: read=0.000 write=0.000
-> Index Only Scan using index_namespaces_on_type_and_id on public.namespaces (cost=0.56..27.59 rows=1 width=4) (actual time=0.000..0.000 rows=0 loops=0)
Index Cond: ((namespaces.type = 'Group'::text) AND (namespaces.id = projects_2.namespace_id))
Heap Fetches: 0
Filter: (NOT (SubPlan 1))
Rows Removed by Filter: 0
I/O Timings: read=0.000 write=0.000
SubPlan 1
-> Index Scan using index_ip_restrictions_on_group_id on public.ip_restrictions (cost=0.29..53.57 rows=44 width=32) (actual time=0.000..0.000 rows=0 loops=0)
Index Cond: (ip_restrictions.group_id = namespaces.id)
I/O Timings: read=0.000 write=0.000
Settings: seq_page_cost = '4', effective_cache_size = '472585MB', jit = 'off', random_page_cost = '1.5', work_mem = '100MB'
Statistics
Time: 16.799 ms
- planning: 15.151 ms
- execution: 1.648 ms
- I/O read: 0.000 ms
- I/O write: 0.000 ms
Shared buffers:
- hits: 1728 (~13.50 MiB) from the buffer pool
- reads: 0 from the OS file cache, including disk I/O
- dirtied: 0
- writes: 0
Screenshots or screen recordings
| Before | After |
|---|---|
![]() |
![]() |
How to set up and validate locally
Scenario:
1. Actors:
User A - private group A(PoC) owner with Gitlab public profile
User B - private group A(PoC) member e.g. role Guest
2. Steps:
- User A - create the private project AP in PoC group A
- User A - create a private snippet APS_1 in the PoC project AP
- User A - in the PoC group A go to
Settings->General->Permissions and group featuresand setRestrict access by IP addressto specified ip address - User B - try to access to the PoC group A from another ip address than specified - you should see 404 not found - expected behaviour
- User B - go to User A public Gitlab profile then open tab
Snippets- notice that you see there the snippet APS_1 - Bypassing "Restrict access by IP address" to view snippets names of the restricted group projects - User A - now, in the project AP create the new private snippet APS_2
- User B - refresh the tab
Snippetsof the public profile of User A - notice that you also see the snippet APS_2 there - Bypassing "Restrict access by IP address" to view snippets names of the restricted group projects
Edited by Emma Park


